@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
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { renderPaddedMarkdownTable, writeJsonMarkdownArtifacts } from "./artifacts.js";
|
|
5
|
+
import { resolveFromRoot } from "./path-utils.js";
|
|
6
|
+
import { runProfiledProcess } from "./process-profile.js";
|
|
7
|
+
import { assertRunCount, percentile } from "./stats.js";
|
|
8
|
+
|
|
9
|
+
const defaultCliPath = fileURLToPath(new URL("./cli.js", import.meta.url));
|
|
10
|
+
|
|
11
|
+
export const defaultImportLoopProfileOptions = {
|
|
12
|
+
entrypoint: "test/fixtures/lazy-import-plugin.mjs",
|
|
13
|
+
generatedAt: "deterministic",
|
|
14
|
+
jsonPath: "reports/plugin-import-loop-profile.json",
|
|
15
|
+
markdownPath: "reports/plugin-import-loop-profile.md",
|
|
16
|
+
outputDir: ".plugin-inspector/import-loop",
|
|
17
|
+
reportTitle: "Plugin Import Loop Profile",
|
|
18
|
+
runs: 3,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export async function buildImportLoopProfile(options = {}) {
|
|
22
|
+
const rootDir = path.resolve(options.rootDir ?? process.cwd());
|
|
23
|
+
const runs = options.runs ?? defaultImportLoopProfileOptions.runs;
|
|
24
|
+
const entrypoint = options.entrypoint ?? defaultImportLoopProfileOptions.entrypoint;
|
|
25
|
+
assertRunCount(runs, 20);
|
|
26
|
+
|
|
27
|
+
const samples = [];
|
|
28
|
+
for (let index = 0; index < runs; index += 1) {
|
|
29
|
+
samples.push(await runCaptureSample({ ...options, entrypoint, index, rootDir }));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const wallMs = samples.map((sample) => sample.wallMs).sort((left, right) => left - right);
|
|
33
|
+
return {
|
|
34
|
+
generatedAt: options.generatedAt ?? defaultImportLoopProfileOptions.generatedAt,
|
|
35
|
+
mode: options.mode ?? "subprocess-cold-import-loop",
|
|
36
|
+
entrypoint,
|
|
37
|
+
summary: {
|
|
38
|
+
runs,
|
|
39
|
+
p50WallMs: percentile(wallMs, 0.5),
|
|
40
|
+
p95WallMs: percentile(wallMs, 0.95),
|
|
41
|
+
maxPeakRssMb: Math.max(0, ...samples.map((sample) => sample.peakRssMb)),
|
|
42
|
+
maxCpuMsEstimate: Math.max(0, ...samples.map((sample) => sample.cpuMsEstimate)),
|
|
43
|
+
capturedCount: samples.reduce((sum, sample) => sum + sample.capturedCount, 0),
|
|
44
|
+
failCount: samples.filter((sample) => sample.exitCode !== 0 || sample.status !== "captured").length,
|
|
45
|
+
},
|
|
46
|
+
samples,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function validateImportLoopProfile(report) {
|
|
51
|
+
const errors = [];
|
|
52
|
+
if (report.summary.failCount > 0) {
|
|
53
|
+
errors.push(`import loop has ${report.summary.failCount} failed sample(s)`);
|
|
54
|
+
}
|
|
55
|
+
if (report.summary.capturedCount < report.summary.runs) {
|
|
56
|
+
errors.push("import loop did not capture at least one contract per run");
|
|
57
|
+
}
|
|
58
|
+
if (report.summary.p50WallMs <= 0) {
|
|
59
|
+
errors.push("import loop is missing wall-time samples");
|
|
60
|
+
}
|
|
61
|
+
return errors;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function writeImportLoopProfile(report, options = {}) {
|
|
65
|
+
const rootDir = path.resolve(options.rootDir ?? process.cwd());
|
|
66
|
+
const jsonPath = resolveFromRoot(rootDir, options.jsonPath ?? defaultImportLoopProfileOptions.jsonPath);
|
|
67
|
+
const markdownPath = resolveFromRoot(rootDir, options.markdownPath ?? defaultImportLoopProfileOptions.markdownPath);
|
|
68
|
+
return writeJsonMarkdownArtifacts({
|
|
69
|
+
jsonPath,
|
|
70
|
+
markdownPath,
|
|
71
|
+
json: report,
|
|
72
|
+
markdown: renderImportLoopProfileMarkdown(report, options),
|
|
73
|
+
check: options.check,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function renderImportLoopProfileMarkdown(report, options = {}) {
|
|
78
|
+
const title = options.title ?? options.reportTitle ?? defaultImportLoopProfileOptions.reportTitle;
|
|
79
|
+
return [
|
|
80
|
+
`# ${title}`,
|
|
81
|
+
"",
|
|
82
|
+
`Generated: ${report.generatedAt}`,
|
|
83
|
+
`Mode: ${report.mode}`,
|
|
84
|
+
`Entrypoint: ${report.entrypoint}`,
|
|
85
|
+
"",
|
|
86
|
+
"## Summary",
|
|
87
|
+
"",
|
|
88
|
+
markdownTable(Object.entries(report.summary).map(([key, value]) => [key, value]), ["Metric", "Value"]),
|
|
89
|
+
"",
|
|
90
|
+
"## Samples",
|
|
91
|
+
"",
|
|
92
|
+
markdownTable(
|
|
93
|
+
report.samples.map((sample) => [
|
|
94
|
+
sample.index,
|
|
95
|
+
sample.status,
|
|
96
|
+
sample.capturedCount,
|
|
97
|
+
`${sample.wallMs} ms`,
|
|
98
|
+
`${sample.peakRssMb} MB`,
|
|
99
|
+
`${sample.cpuMsEstimate} ms`,
|
|
100
|
+
sample.exitCode,
|
|
101
|
+
]),
|
|
102
|
+
["Run", "Status", "Captured", "Wall", "Peak RSS", "CPU Estimate", "Exit"],
|
|
103
|
+
),
|
|
104
|
+
].join("\n");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function runCaptureSample(options) {
|
|
108
|
+
const outputDir = resolveFromRoot(
|
|
109
|
+
options.rootDir,
|
|
110
|
+
options.outputDir ?? defaultImportLoopProfileOptions.outputDir,
|
|
111
|
+
);
|
|
112
|
+
const outputPath = path.join(outputDir, `capture-${options.index}.json`);
|
|
113
|
+
await mkdir(path.dirname(outputPath), { recursive: true });
|
|
114
|
+
|
|
115
|
+
const command = buildCaptureCommand({ ...options, outputPath });
|
|
116
|
+
const profile = await runProfiledProcess({
|
|
117
|
+
command: command.command,
|
|
118
|
+
args: command.args,
|
|
119
|
+
cwd: command.cwd ?? options.rootDir,
|
|
120
|
+
env: { ...process.env, ...command.env },
|
|
121
|
+
});
|
|
122
|
+
const output = profile.exitCode === 0 ? await readCaptureOutput(outputPath) : null;
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
index: options.index,
|
|
126
|
+
exitCode: profile.exitCode,
|
|
127
|
+
status: output?.status ?? "failed",
|
|
128
|
+
capturedCount: output?.captured?.length ?? 0,
|
|
129
|
+
wallMs: profile.wallMs,
|
|
130
|
+
peakRssMb: profile.peakRssMb,
|
|
131
|
+
peakCpuPercent: profile.peakCpuPercent,
|
|
132
|
+
cpuMsEstimate: profile.cpuMsEstimate,
|
|
133
|
+
stderrPreview: profile.stderrPreview,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function buildCaptureCommand(options) {
|
|
138
|
+
if (typeof options.captureCommand === "function") {
|
|
139
|
+
return options.captureCommand({
|
|
140
|
+
entrypoint: options.entrypoint,
|
|
141
|
+
index: options.index,
|
|
142
|
+
outputPath: options.outputPath,
|
|
143
|
+
rootDir: options.rootDir,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
if (options.captureScript) {
|
|
147
|
+
return {
|
|
148
|
+
command: process.execPath,
|
|
149
|
+
args: [options.captureScript, options.entrypoint, "--output", options.outputPath],
|
|
150
|
+
cwd: options.rootDir,
|
|
151
|
+
env: { [options.optInEnv ?? "PLUGIN_INSPECTOR_EXECUTE_ISOLATED"]: "1", ...options.captureEnv },
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
command: process.execPath,
|
|
156
|
+
args: [defaultCliPath, "capture", options.entrypoint, "--output", options.outputPath],
|
|
157
|
+
cwd: options.rootDir,
|
|
158
|
+
env: { PLUGIN_INSPECTOR_EXECUTE_ISOLATED: "1", ...options.captureEnv },
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function readCaptureOutput(outputPath) {
|
|
163
|
+
const { readFile } = await import("node:fs/promises");
|
|
164
|
+
return JSON.parse(await readFile(outputPath, "utf8"));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function markdownTable(rows, headers) {
|
|
168
|
+
return renderPaddedMarkdownTable(rows, headers);
|
|
169
|
+
}
|
package/src/index.js
ADDED
package/src/inspector.js
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import { createCaptureApi } from "./capture-api.js";
|
|
8
|
+
import { fixtureCheckoutPath, fixtureSourceRoot } from "./config.js";
|
|
9
|
+
import { buildCompatibilityFixtureReport } from "./fixture-summary.js";
|
|
10
|
+
import { readOpenClawTargetSurface } from "./openclaw-target.js";
|
|
11
|
+
import { buildCompatibilityReport, buildReport } from "./report.js";
|
|
12
|
+
|
|
13
|
+
const execFileAsync = promisify(execFile);
|
|
14
|
+
|
|
15
|
+
export async function inspectFixtureSet(config, options = {}) {
|
|
16
|
+
const { inspections, failures } = await inspectConfiguredFixtures(config, options);
|
|
17
|
+
return buildReport({ config, inspections, failures, generatedAt: options.generatedAt });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function inspectCompatibilityFixtureSet(config, options = {}) {
|
|
21
|
+
const { inspections, failures } = await inspectConfiguredFixtures(config, options);
|
|
22
|
+
const targetOpenClaw =
|
|
23
|
+
options.targetOpenClaw ??
|
|
24
|
+
(await readOpenClawTargetSurface({
|
|
25
|
+
configuredPath: options.openclawPath,
|
|
26
|
+
manifest: config,
|
|
27
|
+
rootDir: config.rootDir,
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
return buildCompatibilityReport({
|
|
31
|
+
config,
|
|
32
|
+
inspections,
|
|
33
|
+
failures,
|
|
34
|
+
generatedAt: options.generatedAt,
|
|
35
|
+
targetOpenClaw,
|
|
36
|
+
buildFixtureReport: ({ fixture, inspection }) =>
|
|
37
|
+
buildCompatibilityFixtureReport({
|
|
38
|
+
fixture,
|
|
39
|
+
inspection,
|
|
40
|
+
checkoutPath: fixtureCheckoutPath(config, fixture),
|
|
41
|
+
sourceRoot: fixtureSourceRoot(config, fixture),
|
|
42
|
+
rootDir: config.rootDir,
|
|
43
|
+
}),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function inspectConfiguredFixtures(config, options = {}) {
|
|
48
|
+
const inspections = [];
|
|
49
|
+
const failures = [];
|
|
50
|
+
|
|
51
|
+
for (const fixture of config.fixtures) {
|
|
52
|
+
const inspection = await inspectPlugin(fixture, { ...options, config });
|
|
53
|
+
inspections.push(inspection);
|
|
54
|
+
|
|
55
|
+
for (const [key, observed] of [
|
|
56
|
+
["hooks", inspection.hooks],
|
|
57
|
+
["registrations", inspection.registrations],
|
|
58
|
+
["manifestContracts", inspection.manifestContracts],
|
|
59
|
+
]) {
|
|
60
|
+
const expected = fixture.expect?.[key] ?? [];
|
|
61
|
+
const missing = expected.filter((value) => !observed.includes(value));
|
|
62
|
+
if (missing.length > 0) {
|
|
63
|
+
failures.push(`${fixture.id}: missing ${key}: ${missing.join(", ")}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { inspections, failures };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function inspectPlugin(fixture, options = {}) {
|
|
72
|
+
const config = options.config ?? { rootDir: options.rootDir ?? process.cwd() };
|
|
73
|
+
const checkoutPath = fixtureCheckoutPath(config, fixture);
|
|
74
|
+
const sourceRoot = fixtureSourceRoot(config, fixture);
|
|
75
|
+
|
|
76
|
+
if (!existsSync(checkoutPath)) {
|
|
77
|
+
return emptyInspection(fixture, "missing");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const files = await listSourceFiles(sourceRoot, { includeDist: Boolean(fixture.package) });
|
|
81
|
+
if (sourceRoot !== checkoutPath) {
|
|
82
|
+
files.push(...(await listSourceFiles(checkoutPath, { shallowRootOnly: true })));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const hooks = new Set();
|
|
86
|
+
const registrations = new Set();
|
|
87
|
+
const hookDetails = [];
|
|
88
|
+
const registrationDetails = [];
|
|
89
|
+
const sdkImportDetails = [];
|
|
90
|
+
|
|
91
|
+
for (const filePath of files) {
|
|
92
|
+
const text = await readFile(filePath, "utf8");
|
|
93
|
+
const relativePath = path.relative(config.rootDir ?? process.cwd(), filePath);
|
|
94
|
+
const sourceInspection = inspectSourceText(text, relativePath);
|
|
95
|
+
|
|
96
|
+
for (const hook of sourceInspection.hooks) {
|
|
97
|
+
hooks.add(hook.name);
|
|
98
|
+
hookDetails.push(hook);
|
|
99
|
+
}
|
|
100
|
+
for (const registration of sourceInspection.registrations) {
|
|
101
|
+
registrations.add(registration.name);
|
|
102
|
+
registrationDetails.push(registration);
|
|
103
|
+
}
|
|
104
|
+
for (const sdkImport of sourceInspection.sdkImports) {
|
|
105
|
+
sdkImportDetails.push(sdkImport);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const manifestInspection = await readManifestContracts(config, checkoutPath, sourceRoot);
|
|
110
|
+
const packageInspection = await readPackageMetadata(config, checkoutPath, sourceRoot);
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
id: fixture.id,
|
|
114
|
+
status: "ok",
|
|
115
|
+
hooks: [...hooks].sort(),
|
|
116
|
+
hookDetails: sortDetails(hookDetails),
|
|
117
|
+
registrations: [...registrations].sort(),
|
|
118
|
+
registrationDetails: sortDetails(registrationDetails),
|
|
119
|
+
manifestContracts: manifestInspection.contracts,
|
|
120
|
+
manifestFiles: manifestInspection.files,
|
|
121
|
+
manifestErrors: manifestInspection.errors,
|
|
122
|
+
packageFiles: packageInspection.files,
|
|
123
|
+
packageErrors: packageInspection.errors,
|
|
124
|
+
packageEntrypoints: packageInspection.entrypoints,
|
|
125
|
+
sdkImports: uniqueDetails(sdkImportDetails),
|
|
126
|
+
sourceFiles: files.map((filePath) => path.relative(config.rootDir ?? process.cwd(), filePath)).sort(),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function inspectSourceText(text, filePath = "source.js") {
|
|
131
|
+
const searchableText = stripComments(text);
|
|
132
|
+
const hooks = collectDetailedMatches(searchableText, /\bapi\.on\(\s*["'`]([^"'`]+)["'`]/g, filePath, "name");
|
|
133
|
+
const registrations = [
|
|
134
|
+
...collectDetailedMatches(searchableText, /\bapi\.(register[A-Za-z0-9]+)\s*\(/g, filePath, "name"),
|
|
135
|
+
...collectDetailedMatches(searchableText, /\b(defineChannelPluginEntry)\s*\(/g, filePath, "name"),
|
|
136
|
+
...collectDetailedMatches(searchableText, /\b(createChatChannelPlugin)\s*\(/g, filePath, "name"),
|
|
137
|
+
...collectDetailedMatches(searchableText, /\b(definePluginEntry)\s*\(/g, filePath, "name"),
|
|
138
|
+
];
|
|
139
|
+
const sdkImports = collectDetailedMatches(
|
|
140
|
+
searchableText,
|
|
141
|
+
/(?:from\s*["'`]|import\(\s*["'`])([^"'`]*openclaw\/plugin-sdk[^"'`]*)/g,
|
|
142
|
+
filePath,
|
|
143
|
+
"specifier",
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
hooks,
|
|
148
|
+
registrations,
|
|
149
|
+
sdkImports,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function captureEntrypoint(entrypoint, options = {}) {
|
|
154
|
+
if (options.mockSdk === true) {
|
|
155
|
+
return captureEntrypointWithMockSdk(entrypoint, options);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const resolvedEntrypoint = path.resolve(options.cwd ?? process.cwd(), entrypoint);
|
|
159
|
+
const module = await import(pathToFileURL(resolvedEntrypoint).href);
|
|
160
|
+
const register = findRegisterExport(module);
|
|
161
|
+
|
|
162
|
+
if (!register) {
|
|
163
|
+
return {
|
|
164
|
+
status: "no-register-export",
|
|
165
|
+
entrypoint: resolvedEntrypoint,
|
|
166
|
+
captured: [],
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const api = createCaptureApi(options.apiOptions);
|
|
171
|
+
await register(api);
|
|
172
|
+
const result = {
|
|
173
|
+
status: "captured",
|
|
174
|
+
entrypoint: resolvedEntrypoint,
|
|
175
|
+
captured: api.getCapturedContracts(),
|
|
176
|
+
};
|
|
177
|
+
if (options.apiOptions?.retainHandlers === true) {
|
|
178
|
+
result.retained = api.getRetainedContracts();
|
|
179
|
+
}
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export async function captureEntrypointWithMockSdk(entrypoint, options = {}) {
|
|
184
|
+
const runnerPath = fileURLToPath(new URL("./mock-sdk-capture-runner.js", import.meta.url));
|
|
185
|
+
const payload = {
|
|
186
|
+
entrypoint,
|
|
187
|
+
cwd: options.cwd ?? process.cwd(),
|
|
188
|
+
pluginRoot: options.pluginRoot,
|
|
189
|
+
apiOptions: options.apiOptions,
|
|
190
|
+
};
|
|
191
|
+
const { stdout } = await execFileAsync(
|
|
192
|
+
process.execPath,
|
|
193
|
+
["--preserve-symlinks", runnerPath, JSON.stringify(payload)],
|
|
194
|
+
{
|
|
195
|
+
cwd: options.cwd ?? process.cwd(),
|
|
196
|
+
env: {
|
|
197
|
+
...process.env,
|
|
198
|
+
...(options.env ?? {}),
|
|
199
|
+
},
|
|
200
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
201
|
+
},
|
|
202
|
+
);
|
|
203
|
+
return JSON.parse(stdout);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function findRegisterExport(module) {
|
|
207
|
+
if (typeof module.register === "function") {
|
|
208
|
+
return module.register;
|
|
209
|
+
}
|
|
210
|
+
if (typeof module.default === "function") {
|
|
211
|
+
return module.default;
|
|
212
|
+
}
|
|
213
|
+
if (typeof module.default?.register === "function") {
|
|
214
|
+
return module.default.register;
|
|
215
|
+
}
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function emptyInspection(fixture, status) {
|
|
220
|
+
return {
|
|
221
|
+
id: fixture.id,
|
|
222
|
+
status,
|
|
223
|
+
hooks: [],
|
|
224
|
+
hookDetails: [],
|
|
225
|
+
registrations: [],
|
|
226
|
+
registrationDetails: [],
|
|
227
|
+
manifestContracts: [],
|
|
228
|
+
manifestFiles: [],
|
|
229
|
+
manifestErrors: [],
|
|
230
|
+
packageFiles: [],
|
|
231
|
+
packageErrors: [],
|
|
232
|
+
packageEntrypoints: [],
|
|
233
|
+
sdkImports: [],
|
|
234
|
+
sourceFiles: [],
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function collectDetailedMatches(text, regex, filePath, key) {
|
|
239
|
+
const details = [];
|
|
240
|
+
for (const match of text.matchAll(regex)) {
|
|
241
|
+
const line = lineForOffset(text, match.index ?? 0);
|
|
242
|
+
details.push({
|
|
243
|
+
[key]: match[1],
|
|
244
|
+
file: filePath,
|
|
245
|
+
line,
|
|
246
|
+
ref: `${filePath}:${line}`,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
return details;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function readManifestContracts(config, checkoutPath, sourceRoot) {
|
|
253
|
+
const manifests = new Set(
|
|
254
|
+
[path.join(sourceRoot, "openclaw.plugin.json"), path.join(checkoutPath, "openclaw.plugin.json")].filter(
|
|
255
|
+
existsSync,
|
|
256
|
+
),
|
|
257
|
+
);
|
|
258
|
+
const contracts = new Set();
|
|
259
|
+
const files = [];
|
|
260
|
+
const errors = [];
|
|
261
|
+
|
|
262
|
+
for (const manifestFile of manifests) {
|
|
263
|
+
const relativePath = path.relative(config.rootDir ?? process.cwd(), manifestFile);
|
|
264
|
+
files.push(relativePath);
|
|
265
|
+
try {
|
|
266
|
+
const manifest = JSON.parse(await readFile(manifestFile, "utf8"));
|
|
267
|
+
for (const key of Object.keys(manifest.contracts ?? {})) {
|
|
268
|
+
contracts.add(key);
|
|
269
|
+
}
|
|
270
|
+
} catch {
|
|
271
|
+
contracts.add("invalidManifest");
|
|
272
|
+
errors.push(`${relativePath}: invalid JSON`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
contracts: [...contracts].sort(),
|
|
278
|
+
files: files.sort(),
|
|
279
|
+
errors,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function readPackageMetadata(config, checkoutPath, sourceRoot) {
|
|
284
|
+
const packageFiles = new Set(
|
|
285
|
+
[path.join(sourceRoot, "package.json"), path.join(checkoutPath, "package.json")].filter(existsSync),
|
|
286
|
+
);
|
|
287
|
+
const files = [];
|
|
288
|
+
const errors = [];
|
|
289
|
+
const entrypoints = new Set();
|
|
290
|
+
|
|
291
|
+
for (const packageFile of packageFiles) {
|
|
292
|
+
const relativePath = path.relative(config.rootDir ?? process.cwd(), packageFile);
|
|
293
|
+
files.push(relativePath);
|
|
294
|
+
try {
|
|
295
|
+
const packageJson = JSON.parse(await readFile(packageFile, "utf8"));
|
|
296
|
+
collectEntrypoint(entrypoints, packageJson.main);
|
|
297
|
+
collectEntrypoint(entrypoints, packageJson.module);
|
|
298
|
+
collectEntrypoint(entrypoints, packageJson.openclaw?.entry);
|
|
299
|
+
collectEntrypoint(entrypoints, packageJson.openclaw?.entrypoint);
|
|
300
|
+
collectEntrypoint(entrypoints, packageJson.exports?.["."]?.import);
|
|
301
|
+
collectEntrypoint(entrypoints, packageJson.exports?.["."]?.default);
|
|
302
|
+
} catch {
|
|
303
|
+
errors.push(`${relativePath}: invalid JSON`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
files: files.sort(),
|
|
309
|
+
errors,
|
|
310
|
+
entrypoints: [...entrypoints].sort(),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function collectEntrypoint(entrypoints, value) {
|
|
315
|
+
if (typeof value === "string" && value.length > 0) {
|
|
316
|
+
entrypoints.add(value);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async function listSourceFiles(root, options = {}) {
|
|
321
|
+
if (!existsSync(root)) {
|
|
322
|
+
return [];
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const output = [];
|
|
326
|
+
await walk(root, output, options);
|
|
327
|
+
return output;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function walk(dir, output, options) {
|
|
331
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
332
|
+
const entryPath = path.join(dir, entry.name);
|
|
333
|
+
const normalized = entryPath.split(path.sep).join("/");
|
|
334
|
+
|
|
335
|
+
if (entry.isDirectory()) {
|
|
336
|
+
if (shouldSkipDir(entry.name, normalized, options)) {
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (options.shallowRootOnly) {
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
await walk(entryPath, output, options);
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (isSourceFile(entry.name, normalized)) {
|
|
347
|
+
output.push(entryPath);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function shouldSkipDir(name, normalizedPath, options = {}) {
|
|
353
|
+
return (
|
|
354
|
+
name === "node_modules" ||
|
|
355
|
+
(!options.includeDist && name === "dist") ||
|
|
356
|
+
name === "build" ||
|
|
357
|
+
name === "coverage" ||
|
|
358
|
+
name === ".git" ||
|
|
359
|
+
name === "test" ||
|
|
360
|
+
name === "tests" ||
|
|
361
|
+
/\/test-shims\//.test(`${normalizedPath}/`)
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function isSourceFile(name, normalizedPath) {
|
|
366
|
+
return (
|
|
367
|
+
/\.(cjs|mjs|js|ts)$/.test(name) &&
|
|
368
|
+
!name.endsWith(".d.ts") &&
|
|
369
|
+
!/\.test\./.test(name) &&
|
|
370
|
+
!/\.spec\./.test(name)
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function lineForOffset(text, offset) {
|
|
375
|
+
let line = 1;
|
|
376
|
+
for (let index = 0; index < offset; index += 1) {
|
|
377
|
+
if (text.charCodeAt(index) === 10) {
|
|
378
|
+
line += 1;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return line;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function stripComments(text) {
|
|
385
|
+
return text
|
|
386
|
+
.replace(/\/\*[\s\S]*?\*\//g, (comment) => comment.replace(/[^\n]/g, " "))
|
|
387
|
+
.replace(/\/\/.*$/gm, (comment) => " ".repeat(comment.length));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function sortDetails(details) {
|
|
391
|
+
return [...details].sort((left, right) => {
|
|
392
|
+
const leftName = left.name ?? left.specifier ?? "";
|
|
393
|
+
const rightName = right.name ?? right.specifier ?? "";
|
|
394
|
+
return leftName.localeCompare(rightName) || left.ref.localeCompare(right.ref);
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function uniqueDetails(details) {
|
|
399
|
+
const byKey = new Map();
|
|
400
|
+
for (const detail of sortDetails(details)) {
|
|
401
|
+
const key = `${detail.name ?? detail.specifier}:${detail.ref}`;
|
|
402
|
+
byKey.set(key, detail);
|
|
403
|
+
}
|
|
404
|
+
return [...byKey.values()];
|
|
405
|
+
}
|