@openclaw/plugin-inspector 0.3.24 → 0.3.26
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 +31 -0
- package/README.md +141 -0
- package/package.json +4 -2
- package/src/api.js +7 -0
- package/src/artifacts.js +22 -1
- package/src/batch.js +7 -12
- package/src/capture-api.js +15 -0
- package/src/capture-cli.js +1 -1
- package/src/capture-config.js +10 -7
- package/src/cli.js +6 -2
- package/src/cold-import-readiness.js +3 -3
- package/src/fixture-summary.js +43 -6
- package/src/import-loop-profile.js +56 -11
- package/src/inspector.js +120 -99
- package/src/mock-sdk-capture-runner.js +61 -22
- package/src/openclaw-target.js +110 -5
- package/src/openclaw-version.js +121 -9
- package/src/process-profile.js +255 -107
- package/src/runtime-capture-report.js +6 -0
- package/src/runtime-imports.js +191 -0
- package/src/runtime-profile.js +4 -0
- package/src/runtime-reconciliation.js +0 -6
- package/src/sdk-mock.js +128 -13
- package/src/synthetic-entrypoint.js +19 -19
- package/src/synthetic-probes-cli.js +79 -2
- package/src/synthetic-probes.js +212 -34
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { renderPaddedMarkdownTable, writeJsonMarkdownArtifacts } from "./artifacts.js";
|
|
4
|
+
import { readBoundedJsonArtifact, renderPaddedMarkdownTable, writeJsonMarkdownArtifacts } from "./artifacts.js";
|
|
5
5
|
import { resolveFromRoot } from "./path-utils.js";
|
|
6
|
-
import { runProfiledProcess } from "./process-profile.js";
|
|
6
|
+
import { resolveProcessLimits, runProfiledProcess } from "./process-profile.js";
|
|
7
7
|
import { assertRunCount, percentile } from "./stats.js";
|
|
8
8
|
|
|
9
|
-
const
|
|
9
|
+
const defaultRunnerPath = fileURLToPath(new URL("./mock-sdk-capture-runner.js", import.meta.url));
|
|
10
10
|
|
|
11
11
|
export const defaultImportLoopProfileOptions = {
|
|
12
12
|
entrypoint: "test/fixtures/lazy-import-plugin.mjs",
|
|
@@ -255,18 +255,49 @@ async function runCaptureSample(options) {
|
|
|
255
255
|
const outputPath = path.join(outputDir, `${options.sampleName ?? "capture"}-${options.index}.json`);
|
|
256
256
|
await mkdir(path.dirname(outputPath), { recursive: true });
|
|
257
257
|
|
|
258
|
-
const
|
|
258
|
+
const defaultCapture = typeof options.captureCommand !== "function" && !options.captureScript;
|
|
259
|
+
const maxOutputBytes = resolveProcessLimits({
|
|
260
|
+
...options,
|
|
261
|
+
env: { ...process.env, ...options.env, ...options.captureEnv },
|
|
262
|
+
}, "CAPTURE").maxOutputBytes;
|
|
263
|
+
// Only the built-in route owns these sample files. An early process.exit(0)
|
|
264
|
+
// must not turn a previous capture into this run's successful result.
|
|
265
|
+
if (defaultCapture) await rm(outputPath, { force: true });
|
|
266
|
+
const command = buildCaptureCommand({ ...options, outputPath, maxOutputBytes });
|
|
259
267
|
const profile = await runProfiledProcess({
|
|
260
268
|
command: command.command,
|
|
261
269
|
args: command.args,
|
|
262
270
|
cwd: command.cwd ?? options.rootDir,
|
|
263
|
-
env: { ...process.env, ...command.env },
|
|
271
|
+
env: { ...process.env, ...options.env, ...command.env },
|
|
272
|
+
timeoutMs: options.timeoutMs,
|
|
273
|
+
maxOutputBytes: options.maxOutputBytes,
|
|
274
|
+
killGraceMs: options.killGraceMs,
|
|
275
|
+
signal: options.signal,
|
|
264
276
|
});
|
|
265
|
-
|
|
277
|
+
let output = null;
|
|
278
|
+
if (profile.exitCode === 0 && !profile.timedOut && !profile.cancelled) {
|
|
279
|
+
if (defaultCapture) {
|
|
280
|
+
try {
|
|
281
|
+
output = await readCaptureOutput(outputPath, maxOutputBytes);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
profile.exitCode = 1;
|
|
284
|
+
profile.stderrPreview = `Invalid capture artifact: ${error.message}`;
|
|
285
|
+
}
|
|
286
|
+
} else {
|
|
287
|
+
output = await readCaptureOutput(outputPath);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (options.signal?.aborted) {
|
|
291
|
+
profile.exitCode = 1;
|
|
292
|
+
profile.cancelled = true;
|
|
293
|
+
output = null;
|
|
294
|
+
}
|
|
266
295
|
|
|
267
296
|
return {
|
|
268
297
|
index: options.index,
|
|
269
298
|
exitCode: profile.exitCode,
|
|
299
|
+
timedOut: profile.timedOut === true,
|
|
300
|
+
cancelled: profile.cancelled === true,
|
|
270
301
|
status: output?.status ?? "failed",
|
|
271
302
|
capturedCount: output?.captured?.length ?? 0,
|
|
272
303
|
openClawLifecycle: output?.openClawLifecycle ?? null,
|
|
@@ -407,15 +438,29 @@ function buildCaptureCommand(options) {
|
|
|
407
438
|
}
|
|
408
439
|
return {
|
|
409
440
|
command: process.execPath,
|
|
410
|
-
args: [
|
|
441
|
+
args: [
|
|
442
|
+
"--no-warnings",
|
|
443
|
+
"--preserve-symlinks",
|
|
444
|
+
defaultRunnerPath,
|
|
445
|
+
JSON.stringify({
|
|
446
|
+
entrypoint: options.entrypoint,
|
|
447
|
+
cwd: options.rootDir,
|
|
448
|
+
outputPath: options.outputPath,
|
|
449
|
+
maxOutputBytes: options.maxOutputBytes,
|
|
450
|
+
}),
|
|
451
|
+
],
|
|
411
452
|
cwd: options.rootDir,
|
|
412
453
|
env: { PLUGIN_INSPECTOR_EXECUTE_ISOLATED: "1", ...options.captureEnv },
|
|
413
454
|
};
|
|
414
455
|
}
|
|
415
456
|
|
|
416
|
-
async function readCaptureOutput(outputPath) {
|
|
417
|
-
|
|
418
|
-
|
|
457
|
+
async function readCaptureOutput(outputPath, maxOutputBytes) {
|
|
458
|
+
if (maxOutputBytes === undefined) return JSON.parse(await readFile(outputPath, "utf8"));
|
|
459
|
+
const result = await readBoundedJsonArtifact(outputPath, maxOutputBytes);
|
|
460
|
+
if (!result || typeof result.status !== "string" || !Array.isArray(result.captured)) {
|
|
461
|
+
throw new Error("expected a capture status and captured contracts");
|
|
462
|
+
}
|
|
463
|
+
return result;
|
|
419
464
|
}
|
|
420
465
|
|
|
421
466
|
function markdownTable(rows, headers) {
|
package/src/inspector.js
CHANGED
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { execFile } from "node:child_process";
|
|
3
2
|
import { readdir, readFile } from "node:fs/promises";
|
|
4
|
-
import * as nodeModule from "node:module";
|
|
5
3
|
import path from "node:path";
|
|
6
4
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
-
import { promisify } from "node:util";
|
|
8
5
|
import { createCaptureApi } from "./capture-api.js";
|
|
9
6
|
import { captureApiOptionsForPlugin } from "./capture-config.js";
|
|
10
7
|
import { fixtureCheckoutPath, fixtureSourceRoot } from "./config.js";
|
|
11
8
|
import { buildCompatibilityFixtureReport } from "./fixture-summary.js";
|
|
12
9
|
import { readOpenClawTargetSurface } from "./openclaw-target.js";
|
|
13
10
|
import { prepareOpenClawTarget, resolveOpenClawTargetVersion } from "./openclaw-version.js";
|
|
11
|
+
import { resolveProcessLimits, startOwnedProcess } from "./process-profile.js";
|
|
14
12
|
import { buildCompatibilityReport, buildReport } from "./report.js";
|
|
15
13
|
import { inspectSdkDeprecations } from "./sdk-deprecation-rules.js";
|
|
14
|
+
import { collectRuntimeModuleImports } from "./runtime-imports.js";
|
|
16
15
|
|
|
17
|
-
const execFileAsync = promisify(execFile);
|
|
18
16
|
const pluginFactoryNames = "defineBundledChannelEntry|defineChannelPluginEntry|createChatChannelPlugin|definePluginEntry";
|
|
19
17
|
// Bundlers emit unbound calls as (0, sdk.factory)(...), including inline require receivers.
|
|
20
18
|
const compiledFactoryCall = new RegExp(String.raw`\(\s*0\s*,\s*(?:require\s*\(\s*(?:"[^"\r\n]*"|'[^'\r\n]*')\s*\)|[$A-Z_a-z][$\w]*)(?:\s*\.\s*[$A-Z_a-z][$\w]*)*\s*\.\s*(${pluginFactoryNames})\s*\)\s*\(`, "dg");
|
|
@@ -29,6 +27,13 @@ export async function inspectFixtureSet(config, options = {}) {
|
|
|
29
27
|
|
|
30
28
|
export async function inspectCompatibilityFixtureSet(config, options = {}) {
|
|
31
29
|
const { inspections, failures } = await inspectConfiguredFixtures(config, options);
|
|
30
|
+
const reportConfig = {
|
|
31
|
+
...config,
|
|
32
|
+
fixtures: config.fixtures.map((fixture) => ({
|
|
33
|
+
...fixture,
|
|
34
|
+
checkoutPath: normalizeFixtureCheckoutPath(config, fixture),
|
|
35
|
+
})),
|
|
36
|
+
};
|
|
32
37
|
const targetOpenClaw =
|
|
33
38
|
options.targetOpenClaw ??
|
|
34
39
|
(options.openclawVersion
|
|
@@ -40,7 +45,7 @@ export async function inspectCompatibilityFixtureSet(config, options = {}) {
|
|
|
40
45
|
}));
|
|
41
46
|
|
|
42
47
|
return buildCompatibilityReport({
|
|
43
|
-
config,
|
|
48
|
+
config: reportConfig,
|
|
44
49
|
inspections,
|
|
45
50
|
failures,
|
|
46
51
|
authorFacing: options.authorFacing,
|
|
@@ -58,6 +63,11 @@ export async function inspectCompatibilityFixtureSet(config, options = {}) {
|
|
|
58
63
|
});
|
|
59
64
|
}
|
|
60
65
|
|
|
66
|
+
function normalizeFixtureCheckoutPath(config, fixture) {
|
|
67
|
+
const relative = path.relative(config.rootDir ?? process.cwd(), fixtureCheckoutPath(config, fixture));
|
|
68
|
+
return (relative || ".").replaceAll("\\", "/");
|
|
69
|
+
}
|
|
70
|
+
|
|
61
71
|
async function inspectConfiguredFixtures(config, options = {}) {
|
|
62
72
|
const inspections = [];
|
|
63
73
|
const failures = [];
|
|
@@ -170,7 +180,7 @@ export function inspectSourceText(text, filePath = "source.js") {
|
|
|
170
180
|
...collectDetailedMatches(searchableText, new RegExp(String.raw`\b(${pluginFactoryNames})\s*\(`, "g"), filePath, "name"),
|
|
171
181
|
...collectDetailedMatches(searchableText, compiledFactoryCall, filePath, "name"),
|
|
172
182
|
];
|
|
173
|
-
const sdkImports = collectSdkImports(searchableText, filePath);
|
|
183
|
+
const sdkImports = collectSdkImports(searchableText, filePath, text);
|
|
174
184
|
const sdkDeprecations = inspectSdkDeprecations(searchableText, filePath);
|
|
175
185
|
|
|
176
186
|
return {
|
|
@@ -185,14 +195,23 @@ export async function captureEntrypoint(entrypoint, options = {}) {
|
|
|
185
195
|
if (options.mockSdk === true) {
|
|
186
196
|
return captureEntrypointWithMockSdk(entrypoint, options);
|
|
187
197
|
}
|
|
198
|
+
if (options.isolateCapture === true) {
|
|
199
|
+
return captureEntrypointInChild(entrypoint, { ...options, mockSdk: false });
|
|
200
|
+
}
|
|
201
|
+
return invokeWithTimeout((signal) => captureInProcess(entrypoint, options, signal), options);
|
|
202
|
+
}
|
|
188
203
|
|
|
204
|
+
async function captureInProcess(entrypoint, options, signal) {
|
|
205
|
+
signal.throwIfAborted();
|
|
189
206
|
const resolvedEntrypoint = path.resolve(options.cwd ?? process.cwd(), entrypoint);
|
|
190
207
|
let module;
|
|
191
208
|
try {
|
|
192
209
|
module = await import(pathToFileURL(resolvedEntrypoint).href);
|
|
193
210
|
} catch (error) {
|
|
211
|
+
signal.throwIfAborted();
|
|
194
212
|
throw classifyCapturePhaseError(error, "entrypoint-import-error");
|
|
195
213
|
}
|
|
214
|
+
signal.throwIfAborted();
|
|
196
215
|
const register = findRegisterExport(module);
|
|
197
216
|
|
|
198
217
|
if (!register) {
|
|
@@ -207,13 +226,17 @@ export async function captureEntrypoint(entrypoint, options = {}) {
|
|
|
207
226
|
pluginRoot: options.pluginRoot
|
|
208
227
|
? path.resolve(options.cwd ?? process.cwd(), options.pluginRoot)
|
|
209
228
|
: path.dirname(resolvedEntrypoint),
|
|
229
|
+
signal,
|
|
210
230
|
});
|
|
231
|
+
signal.throwIfAborted();
|
|
211
232
|
const api = createCaptureApi(apiOptions);
|
|
212
233
|
try {
|
|
213
234
|
await register(api);
|
|
214
235
|
} catch (error) {
|
|
236
|
+
signal.throwIfAborted();
|
|
215
237
|
throw classifyCapturePhaseError(error, "registration-execution-error");
|
|
216
238
|
}
|
|
239
|
+
signal.throwIfAborted();
|
|
217
240
|
const result = {
|
|
218
241
|
status: "captured",
|
|
219
242
|
entrypoint: resolvedEntrypoint,
|
|
@@ -226,60 +249,68 @@ export async function captureEntrypoint(entrypoint, options = {}) {
|
|
|
226
249
|
}
|
|
227
250
|
|
|
228
251
|
export async function captureEntrypointWithMockSdk(entrypoint, options = {}) {
|
|
252
|
+
return captureEntrypointInChild(entrypoint, { ...options, mockSdk: true });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function captureEntrypointInChild(entrypoint, options) {
|
|
256
|
+
const label = options.mockSdk ? "Mock SDK" : "Real SDK";
|
|
229
257
|
const runnerPath = fileURLToPath(new URL("./mock-sdk-capture-runner.js", import.meta.url));
|
|
230
258
|
const payload = {
|
|
231
259
|
entrypoint,
|
|
260
|
+
mockSdk: options.mockSdk,
|
|
232
261
|
cwd: options.cwd ?? process.cwd(),
|
|
233
262
|
pluginRoot: options.pluginRoot,
|
|
234
263
|
apiOptions: options.apiOptions,
|
|
264
|
+
maxOutputBytes: options.maxOutputBytes,
|
|
235
265
|
};
|
|
266
|
+
const { result } = startOwnedProcess({
|
|
267
|
+
command: process.execPath,
|
|
268
|
+
args: ["--no-warnings", ...(options.mockSdk ? ["--preserve-symlinks"] : []), runnerPath, JSON.stringify(payload)],
|
|
269
|
+
cwd: options.cwd ?? process.cwd(),
|
|
270
|
+
env: { ...process.env, ...options.env },
|
|
271
|
+
timeoutMs: options.timeoutMs,
|
|
272
|
+
killGraceMs: options.killGraceMs,
|
|
273
|
+
maxOutputBytes: options.maxOutputBytes,
|
|
274
|
+
signal: options.signal,
|
|
275
|
+
}, "CAPTURE");
|
|
276
|
+
const outcome = await result;
|
|
277
|
+
if (outcome.exitCode !== 0 || outcome.outputTruncated) {
|
|
278
|
+
const message = outcome.cancelled ? `${label} capture cancelled`
|
|
279
|
+
: outcome.outputTruncated ? `${label} capture output exceeded its byte limit`
|
|
280
|
+
: `${label} capture child failed`;
|
|
281
|
+
const { error: childError, ...details } = outcome;
|
|
282
|
+
throw classifyChildCaptureError(Object.assign(childError ?? new Error(message), details), options.mockSdk);
|
|
283
|
+
}
|
|
236
284
|
try {
|
|
237
|
-
|
|
238
|
-
process.execPath,
|
|
239
|
-
["--no-warnings", "--preserve-symlinks", runnerPath, JSON.stringify(payload)],
|
|
240
|
-
{
|
|
241
|
-
cwd: options.cwd ?? process.cwd(),
|
|
242
|
-
env: {
|
|
243
|
-
...process.env,
|
|
244
|
-
...(options.env ?? {}),
|
|
245
|
-
},
|
|
246
|
-
maxBuffer: 1024 * 1024 * 10,
|
|
247
|
-
},
|
|
248
|
-
);
|
|
249
|
-
return JSON.parse(stdout);
|
|
285
|
+
return JSON.parse(outcome.stdout);
|
|
250
286
|
} catch (error) {
|
|
251
|
-
|
|
252
|
-
if (captured) {
|
|
253
|
-
return captured;
|
|
254
|
-
}
|
|
255
|
-
throw classifyMockSdkCaptureError(error);
|
|
287
|
+
throw classifyChildCaptureError(error, options.mockSdk);
|
|
256
288
|
}
|
|
257
289
|
}
|
|
258
290
|
|
|
259
|
-
function
|
|
260
|
-
|
|
261
|
-
|
|
291
|
+
export function classifyMockSdkCaptureError(error) {
|
|
292
|
+
return classifyChildCaptureError(error, true);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function classifyChildCaptureError(error, mockSdk) {
|
|
296
|
+
const label = mockSdk ? "Mock SDK" : "Real SDK";
|
|
297
|
+
const fallbackClass = mockSdk ? "mock-sdk-capture-error" : "capture-error";
|
|
298
|
+
if (error?.timedOut === true) {
|
|
299
|
+
return enrichCaptureError(error, {
|
|
300
|
+
message: `${label} capture timed out after ${error.timeoutMs}ms`,
|
|
301
|
+
failureClass: "capture-timeout",
|
|
302
|
+
});
|
|
262
303
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
typeof parsed.status === "string" &&
|
|
269
|
-
Array.isArray(parsed.captured)
|
|
270
|
-
) {
|
|
271
|
-
return parsed;
|
|
272
|
-
}
|
|
273
|
-
} catch {
|
|
274
|
-
return null;
|
|
304
|
+
if (error?.cancelled || error?.outputTruncated) {
|
|
305
|
+
return enrichCaptureError(error, {
|
|
306
|
+
message: error.message,
|
|
307
|
+
failureClass: fallbackClass,
|
|
308
|
+
});
|
|
275
309
|
}
|
|
276
|
-
return null;
|
|
277
|
-
}
|
|
278
310
|
|
|
279
|
-
export function classifyMockSdkCaptureError(error) {
|
|
280
311
|
const rawMessage = [error?.stderr, error?.stdout, error?.message].filter(Boolean).join("\n");
|
|
281
312
|
const missingExport = rawMessage.match(/does not provide an export named ['"]([^'"]+)['"]/)?.[1];
|
|
282
|
-
if (missingExport) {
|
|
313
|
+
if (mockSdk && missingExport) {
|
|
283
314
|
return enrichCaptureError(error, {
|
|
284
315
|
message: `Mock SDK import failed: openclaw/plugin-sdk is missing export ${missingExport}`,
|
|
285
316
|
failureClass: "missing-sdk-export",
|
|
@@ -290,9 +321,9 @@ export function classifyMockSdkCaptureError(error) {
|
|
|
290
321
|
const missingModule =
|
|
291
322
|
rawMessage.match(/Cannot find (?:package|module) ['"]([^'"]*openclaw\/plugin-sdk[^'"]*)['"]/)?.[1] ??
|
|
292
323
|
rawMessage.match(/Package subpath ['"](\.\/plugin-sdk\/[^'"]+)['"]/)?.[1];
|
|
293
|
-
if (
|
|
324
|
+
if (mockSdk && missingModule) {
|
|
294
325
|
return enrichCaptureError(error, {
|
|
295
|
-
message: `Mock SDK import failed: ${missingModule
|
|
326
|
+
message: `Mock SDK import failed: ${missingModule}`,
|
|
296
327
|
failureClass: "missing-sdk-module",
|
|
297
328
|
missingModule,
|
|
298
329
|
});
|
|
@@ -301,15 +332,50 @@ export function classifyMockSdkCaptureError(error) {
|
|
|
301
332
|
const failureClass = rawMessage.match(/\[plugin-inspector:([^\]]+)\]/)?.[1];
|
|
302
333
|
if (failureClass) {
|
|
303
334
|
return enrichCaptureError(error, {
|
|
304
|
-
message: firstMeaningfulErrorLine(rawMessage.replace(/\[plugin-inspector:[^\]]+\]/, "")) ??
|
|
335
|
+
message: firstMeaningfulErrorLine(rawMessage.replace(/\[plugin-inspector:[^\]]+\]/, "")) ?? `${label} capture failed`,
|
|
305
336
|
failureClass,
|
|
306
337
|
});
|
|
307
338
|
}
|
|
339
|
+
if (mockSdk && rawMessage.includes("openclaw/plugin-sdk")) {
|
|
340
|
+
return enrichCaptureError(error, {
|
|
341
|
+
message: "Mock SDK import failed: openclaw/plugin-sdk module could not be resolved",
|
|
342
|
+
failureClass: "missing-sdk-module",
|
|
343
|
+
});
|
|
344
|
+
}
|
|
308
345
|
|
|
309
346
|
return enrichCaptureError(error, {
|
|
310
|
-
message: firstMeaningfulErrorLine(rawMessage) ??
|
|
311
|
-
failureClass:
|
|
347
|
+
message: firstMeaningfulErrorLine(rawMessage) ?? `${label} capture failed`,
|
|
348
|
+
failureClass: fallbackClass,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function invokeWithTimeout(invoke, options) {
|
|
353
|
+
const { timeoutMs } = resolveProcessLimits(options, "CAPTURE");
|
|
354
|
+
const controller = new AbortController();
|
|
355
|
+
let rejectAborted;
|
|
356
|
+
const aborted = new Promise((_, reject) => { rejectAborted = reject; });
|
|
357
|
+
const onAbort = () => rejectAborted(controller.signal.reason);
|
|
358
|
+
const onCancel = () => controller.abort(classifyCapturePhaseError(
|
|
359
|
+
new Error("In-process capture cancelled"), "capture-error",
|
|
360
|
+
));
|
|
361
|
+
controller.signal.addEventListener("abort", onAbort, { once: true });
|
|
362
|
+
const timeoutId = setTimeout(() => controller.abort(classifyCapturePhaseError(
|
|
363
|
+
new Error(`In-process capture timed out after ${timeoutMs}ms`), "capture-timeout",
|
|
364
|
+
)), timeoutMs);
|
|
365
|
+
// Observe late settlement, but stop inspector-owned phases after the deadline.
|
|
366
|
+
// Arbitrary plugin JavaScript is only preemptable in the supervised CLI child.
|
|
367
|
+
const run = Promise.resolve().then(() => {
|
|
368
|
+
controller.signal.throwIfAborted();
|
|
369
|
+
return invoke(controller.signal);
|
|
370
|
+
});
|
|
371
|
+
const result = Promise.race([run, aborted]).finally(() => {
|
|
372
|
+
clearTimeout(timeoutId);
|
|
373
|
+
controller.signal.removeEventListener("abort", onAbort);
|
|
374
|
+
options.signal?.removeEventListener("abort", onCancel);
|
|
312
375
|
});
|
|
376
|
+
options.signal?.addEventListener("abort", onCancel, { once: true });
|
|
377
|
+
if (options.signal?.aborted) onCancel();
|
|
378
|
+
return result;
|
|
313
379
|
}
|
|
314
380
|
|
|
315
381
|
export function classifyCapturePhaseError(error, failureClass) {
|
|
@@ -385,7 +451,7 @@ function collectDetailedMatches(text, regex, filePath, key) {
|
|
|
385
451
|
return details;
|
|
386
452
|
}
|
|
387
453
|
|
|
388
|
-
function collectSdkImports(text, filePath) {
|
|
454
|
+
function collectSdkImports(text, filePath, sourceText) {
|
|
389
455
|
const details = [];
|
|
390
456
|
for (const candidate of text.matchAll(/(?:^|;)[\t ]*(import|export)\b/gm)) {
|
|
391
457
|
const keyword = candidate[1];
|
|
@@ -402,13 +468,12 @@ function collectSdkImports(text, filePath) {
|
|
|
402
468
|
});
|
|
403
469
|
}
|
|
404
470
|
|
|
405
|
-
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
const line = lineForOffset(text, match.index ?? 0);
|
|
471
|
+
// Comment masking can erase executable interpolations after URL-like template text.
|
|
472
|
+
for (const { specifier, index } of collectRuntimeModuleImports(sourceText)) {
|
|
473
|
+
if (specifier !== "openclaw/plugin-sdk" && !specifier.startsWith("openclaw/plugin-sdk/")) continue;
|
|
474
|
+
const line = lineForOffset(text, index);
|
|
410
475
|
details.push({
|
|
411
|
-
specifier
|
|
476
|
+
specifier,
|
|
412
477
|
file: filePath,
|
|
413
478
|
line,
|
|
414
479
|
ref: `${filePath}:${line}`,
|
|
@@ -475,50 +540,6 @@ function skipQuotedText(text, quoteIndex, quote) {
|
|
|
475
540
|
return cursor;
|
|
476
541
|
}
|
|
477
542
|
|
|
478
|
-
function runtimeDynamicImportIndexes(text, matches) {
|
|
479
|
-
if (matches.length === 0) return new Set();
|
|
480
|
-
const markedImports = matches.map((match, index) => {
|
|
481
|
-
const specifier = match[1];
|
|
482
|
-
const specifierStart = (match.index ?? 0) + match[0].indexOf(specifier);
|
|
483
|
-
return {
|
|
484
|
-
index,
|
|
485
|
-
specifierStart,
|
|
486
|
-
specifierEnd: specifierStart + specifier.length,
|
|
487
|
-
marker: `${specifier}/__plugin_inspector_runtime_import_${index}__`,
|
|
488
|
-
};
|
|
489
|
-
});
|
|
490
|
-
let markedText = text;
|
|
491
|
-
for (const markedImport of markedImports.toReversed()) {
|
|
492
|
-
markedText =
|
|
493
|
-
markedText.slice(0, markedImport.specifierStart) +
|
|
494
|
-
markedImport.marker +
|
|
495
|
-
markedText.slice(markedImport.specifierEnd);
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
try {
|
|
499
|
-
const runtimeText = eraseTypeScript(markedText);
|
|
500
|
-
if (runtimeText === null) {
|
|
501
|
-
return new Set(markedImports.map((markedImport) => markedImport.index));
|
|
502
|
-
}
|
|
503
|
-
return new Set(
|
|
504
|
-
markedImports.filter((markedImport) => runtimeText.includes(markedImport.marker)).map((markedImport) => markedImport.index),
|
|
505
|
-
);
|
|
506
|
-
} catch {
|
|
507
|
-
return new Set(markedImports.map((markedImport) => markedImport.index));
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
function eraseTypeScript(text) {
|
|
512
|
-
if (typeof nodeModule.stripTypeScriptTypes === "function") {
|
|
513
|
-
return nodeModule.stripTypeScriptTypes(text, { mode: "transform" });
|
|
514
|
-
}
|
|
515
|
-
if (typeof globalThis.Bun?.Transpiler === "function") {
|
|
516
|
-
const transpiler = new globalThis.Bun.Transpiler({ loader: "ts", target: "bun" });
|
|
517
|
-
return transpiler.transformSync(text);
|
|
518
|
-
}
|
|
519
|
-
return null;
|
|
520
|
-
}
|
|
521
|
-
|
|
522
543
|
function isTypeOnlyStaticImportClause(clause) {
|
|
523
544
|
clause = clause.trim();
|
|
524
545
|
if (/^type\b/.test(clause)) {
|
|
@@ -1,37 +1,56 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { rmSync } from "node:fs";
|
|
3
3
|
import { mkdtemp } from "node:fs/promises";
|
|
4
|
-
import { register } from "node:module";
|
|
5
4
|
import os from "node:os";
|
|
6
5
|
import path from "node:path";
|
|
7
6
|
import { pathToFileURL } from "node:url";
|
|
7
|
+
import { writeArtifacts } from "./artifacts.js";
|
|
8
8
|
import { createCaptureApi } from "./capture-api.js";
|
|
9
9
|
import { captureApiOptionsForPlugin } from "./capture-config.js";
|
|
10
|
-
import {
|
|
10
|
+
import { createCappedCollector, resolveProcessLimits } from "./process-profile.js";
|
|
11
|
+
import { createMockSdkPackage, installMockSdkLoader } from "./sdk-mock.js";
|
|
11
12
|
|
|
12
13
|
const options = JSON.parse(process.argv[2] ?? "{}");
|
|
13
14
|
let activeOutputCapture = null;
|
|
14
15
|
|
|
15
16
|
try {
|
|
16
17
|
const result = await run(options);
|
|
17
|
-
|
|
18
|
+
const json = `${JSON.stringify(result, null, 2)}\n`;
|
|
19
|
+
const { maxOutputBytes } = resolveProcessLimits(options, options.syntheticProbes ? "PROBE" : "CAPTURE");
|
|
20
|
+
if (Buffer.byteLength(json) > maxOutputBytes) {
|
|
21
|
+
const label = options.syntheticProbes ? "Synthetic probe" : `${options.mockSdk === false ? "Real SDK" : "Mock SDK"} capture`;
|
|
22
|
+
throw new Error(`${label} result exceeded its ${maxOutputBytes}-byte limit`);
|
|
23
|
+
}
|
|
24
|
+
if (options.outputPath) {
|
|
25
|
+
await writeArtifacts([{ path: options.outputPath, content: json }]);
|
|
26
|
+
} else {
|
|
27
|
+
await writeRunnerStdout(json);
|
|
28
|
+
}
|
|
29
|
+
process.exit(0);
|
|
18
30
|
} catch (error) {
|
|
19
31
|
if (error.failureClass) {
|
|
20
|
-
writeRunnerStderr(`[plugin-inspector:${error.failureClass}]\n`);
|
|
32
|
+
await writeRunnerStderr(`[plugin-inspector:${error.failureClass}]\n`);
|
|
21
33
|
}
|
|
22
|
-
writeRunnerStderr(`${error.stack ?? error.message}\n`);
|
|
23
|
-
process.
|
|
34
|
+
await writeRunnerStderr(`${options.outputPath ? error.message : (error.stack ?? error.message)}\n`);
|
|
35
|
+
process.exit(1);
|
|
24
36
|
}
|
|
25
37
|
|
|
26
38
|
async function run(options) {
|
|
27
39
|
const entrypoint = path.resolve(options.cwd ?? process.cwd(), options.entrypoint);
|
|
28
40
|
const pluginRoot = path.resolve(options.cwd ?? process.cwd(), options.pluginRoot ?? path.dirname(entrypoint));
|
|
41
|
+
if (options.mockSdk === false) {
|
|
42
|
+
return await captureLinkedEntrypoint(entrypoint, { ...options, pluginRoot });
|
|
43
|
+
}
|
|
29
44
|
const workspace = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-mock-sdk-"));
|
|
30
45
|
|
|
31
46
|
cleanupTempDirOnExit(workspace);
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
47
|
+
const mockPackage = await createMockSdkPackage(workspace, { pluginRoot });
|
|
48
|
+
const stopLoader = await installMockSdkLoader(mockPackage);
|
|
49
|
+
try {
|
|
50
|
+
return await captureLinkedEntrypoint(entrypoint, { ...options, pluginRoot });
|
|
51
|
+
} finally {
|
|
52
|
+
stopLoader();
|
|
53
|
+
}
|
|
35
54
|
}
|
|
36
55
|
|
|
37
56
|
function cleanupTempDirOnExit(dir) {
|
|
@@ -55,14 +74,15 @@ async function captureLinkedEntrypoint(entrypoint, options) {
|
|
|
55
74
|
|
|
56
75
|
if (!register) {
|
|
57
76
|
await drainAsyncOutput();
|
|
58
|
-
return
|
|
77
|
+
return finishCapture(
|
|
59
78
|
{
|
|
60
79
|
status: "no-register-export",
|
|
61
|
-
entrypoint: options.entrypoint,
|
|
62
|
-
mockSdk:
|
|
80
|
+
entrypoint: options.mockSdk === false ? entrypoint : options.entrypoint,
|
|
81
|
+
mockSdk: options.mockSdk !== false,
|
|
63
82
|
captured: [],
|
|
64
83
|
},
|
|
65
84
|
outputCapture,
|
|
85
|
+
options,
|
|
66
86
|
);
|
|
67
87
|
}
|
|
68
88
|
|
|
@@ -80,13 +100,22 @@ async function captureLinkedEntrypoint(entrypoint, options) {
|
|
|
80
100
|
|
|
81
101
|
const result = {
|
|
82
102
|
status: "captured",
|
|
83
|
-
entrypoint: options.entrypoint,
|
|
84
|
-
mockSdk:
|
|
103
|
+
entrypoint: options.mockSdk === false ? entrypoint : options.entrypoint,
|
|
104
|
+
mockSdk: options.mockSdk !== false,
|
|
85
105
|
captured: api.getCapturedContracts(),
|
|
86
106
|
};
|
|
87
107
|
if (apiOptions?.retainHandlers === true) {
|
|
88
108
|
result.retained = api.getRetainedContracts();
|
|
89
109
|
}
|
|
110
|
+
return finishCapture(result, outputCapture, options);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function finishCapture(result, outputCapture, options) {
|
|
114
|
+
if (options.syntheticProbes) {
|
|
115
|
+
const { runCapturedSyntheticProbes } = await import("./synthetic-probes.js");
|
|
116
|
+
result = await runCapturedSyntheticProbes(result, options);
|
|
117
|
+
await drainAsyncOutput();
|
|
118
|
+
}
|
|
90
119
|
return withProcessOutput(result, outputCapture);
|
|
91
120
|
}
|
|
92
121
|
|
|
@@ -124,18 +153,18 @@ function findRegisterExport(module) {
|
|
|
124
153
|
}
|
|
125
154
|
|
|
126
155
|
function installProcessOutputCapture() {
|
|
127
|
-
const
|
|
128
|
-
const
|
|
156
|
+
const stdout = createCappedCollector(1024 * 1024);
|
|
157
|
+
const stderr = createCappedCollector(1024 * 1024);
|
|
129
158
|
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
130
159
|
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
131
160
|
|
|
132
161
|
process.stdout.write = (chunk, encoding, callback) => {
|
|
133
|
-
|
|
162
|
+
stdout.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), typeof encoding === "string" ? encoding : "utf8"));
|
|
134
163
|
invokeWriteCallback(encoding, callback);
|
|
135
164
|
return true;
|
|
136
165
|
};
|
|
137
166
|
process.stderr.write = (chunk, encoding, callback) => {
|
|
138
|
-
|
|
167
|
+
stderr.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), typeof encoding === "string" ? encoding : "utf8"));
|
|
139
168
|
invokeWriteCallback(encoding, callback);
|
|
140
169
|
return true;
|
|
141
170
|
};
|
|
@@ -143,8 +172,8 @@ function installProcessOutputCapture() {
|
|
|
143
172
|
return {
|
|
144
173
|
originalStdoutWrite,
|
|
145
174
|
originalStderrWrite,
|
|
146
|
-
stdout: () =>
|
|
147
|
-
stderr: () =>
|
|
175
|
+
stdout: () => stdout.text(),
|
|
176
|
+
stderr: () => stderr.text(),
|
|
148
177
|
};
|
|
149
178
|
}
|
|
150
179
|
|
|
@@ -162,9 +191,19 @@ async function drainAsyncOutput() {
|
|
|
162
191
|
}
|
|
163
192
|
|
|
164
193
|
function writeRunnerStdout(text) {
|
|
165
|
-
|
|
194
|
+
const write = activeOutputCapture?.originalStdoutWrite ?? process.stdout.write.bind(process.stdout);
|
|
195
|
+
return flushWrite(write, text);
|
|
166
196
|
}
|
|
167
197
|
|
|
168
198
|
function writeRunnerStderr(text) {
|
|
169
|
-
|
|
199
|
+
const write = activeOutputCapture?.originalStderrWrite ?? process.stderr.write.bind(process.stderr);
|
|
200
|
+
return flushWrite(write, text);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// The runner exits deliberately to shed plugin timers, but only after the
|
|
204
|
+
// complete protocol response has reached its pipe.
|
|
205
|
+
function flushWrite(write, text) {
|
|
206
|
+
return new Promise((resolve, reject) => {
|
|
207
|
+
write(text, (error) => error ? reject(error) : resolve());
|
|
208
|
+
});
|
|
170
209
|
}
|