@openclaw/plugin-inspector 0.1.3 → 0.3.0

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.
@@ -0,0 +1,235 @@
1
+ import path from "node:path";
2
+ import { writeArtifacts } from "./artifacts.js";
3
+
4
+ export const defaultSarifPath = "plugin-inspector.sarif";
5
+ export const defaultJunitPath = "plugin-inspector.junit.xml";
6
+
7
+ export async function writeCiOutputArtifacts(report, options = {}) {
8
+ const outDir = path.resolve(options.cwd ?? process.cwd(), options.outDir ?? "reports");
9
+ const artifacts = [];
10
+
11
+ if (options.sarifPath) {
12
+ artifacts.push({
13
+ name: "sarifPath",
14
+ path: path.resolve(outDir, options.sarifPath),
15
+ json: buildSarifReport(report),
16
+ });
17
+ }
18
+
19
+ if (options.junitPath) {
20
+ artifacts.push({
21
+ name: "junitPath",
22
+ path: path.resolve(outDir, options.junitPath),
23
+ content: renderJunitXml(report),
24
+ });
25
+ }
26
+
27
+ if (artifacts.length === 0) {
28
+ return {};
29
+ }
30
+
31
+ return writeArtifacts(artifacts, { check: options.check });
32
+ }
33
+
34
+ export function buildSarifReport(report) {
35
+ const findings = reportFindings(report);
36
+ const rules = [...new Map(findings.map((finding) => [finding.code, sarifRule(finding)])).values()];
37
+ const fixtureById = new Map((report.fixtures ?? []).map((fixture) => [fixture.id, fixture]));
38
+
39
+ return {
40
+ $schema: "https://json.schemastore.org/sarif-2.1.0.json",
41
+ version: "2.1.0",
42
+ runs: [
43
+ {
44
+ tool: {
45
+ driver: {
46
+ name: "plugin-inspector",
47
+ informationUri: "https://github.com/openclaw/plugin-inspector",
48
+ rules,
49
+ },
50
+ },
51
+ results: findings.map((finding) => sarifResult(finding, fixtureById)),
52
+ },
53
+ ],
54
+ };
55
+ }
56
+
57
+ export function renderJunitXml(report) {
58
+ const findings = reportFindings(report);
59
+ const testcases = findings.length > 0 ? findings.map(junitFindingTestcase) : [junitPassingTestcase(report)];
60
+ const failures = findings.filter(isBlockingFinding).length;
61
+ const tests = testcases.length;
62
+
63
+ return [
64
+ '<?xml version="1.0" encoding="UTF-8"?>',
65
+ `<testsuite name="plugin-inspector" tests="${tests}" failures="${failures}" errors="0" skipped="0">`,
66
+ ...testcases,
67
+ "</testsuite>",
68
+ "",
69
+ ].join("\n");
70
+ }
71
+
72
+ export function reportFindings(report) {
73
+ const findings = new Map();
74
+ for (const finding of [...(report.breakages ?? []), ...(report.warnings ?? []), ...(report.suggestions ?? [])]) {
75
+ findings.set(findingKey(finding), finding);
76
+ }
77
+ for (const issue of report.issues ?? []) {
78
+ const finding = issueToFinding(issue);
79
+ findings.set(findingKey(finding), {
80
+ ...findings.get(findingKey(finding)),
81
+ ...finding,
82
+ });
83
+ }
84
+ return [...findings.values()];
85
+ }
86
+
87
+ function issueToFinding(issue) {
88
+ return {
89
+ fixture: issue.fixture,
90
+ code: issue.code,
91
+ level: issue.status === "blocking" ? "breakage" : "warning",
92
+ message: issue.title,
93
+ evidence: issue.evidence ?? [],
94
+ severity: issue.severity,
95
+ issueClass: issue.issueClass,
96
+ };
97
+ }
98
+
99
+ function findingKey(finding) {
100
+ return [
101
+ finding.fixture ?? "",
102
+ finding.code ?? "",
103
+ ...normalizeEvidence(finding.evidence),
104
+ ].join("\n");
105
+ }
106
+
107
+ function sarifRule(finding) {
108
+ return {
109
+ id: finding.code,
110
+ shortDescription: {
111
+ text: finding.code,
112
+ },
113
+ fullDescription: {
114
+ text: finding.message ?? finding.code,
115
+ },
116
+ defaultConfiguration: {
117
+ level: sarifLevel(finding),
118
+ },
119
+ };
120
+ }
121
+
122
+ function sarifResult(finding, fixtureById) {
123
+ return {
124
+ ruleId: finding.code,
125
+ level: sarifLevel(finding),
126
+ message: {
127
+ text: finding.message ?? finding.code,
128
+ },
129
+ locations: [sarifLocation(finding, fixtureById)],
130
+ properties: {
131
+ fixture: finding.fixture,
132
+ severity: finding.severity ?? finding.level,
133
+ issueClass: finding.issueClass,
134
+ evidence: normalizeEvidence(finding.evidence),
135
+ },
136
+ };
137
+ }
138
+
139
+ function sarifLocation(finding, fixtureById) {
140
+ const parsed = parseEvidenceLocation(normalizeEvidence(finding.evidence)[0]);
141
+ const fixture = fixtureById.get(finding.fixture);
142
+ const uri = parsed?.uri ?? fixture?.path ?? ".";
143
+ return {
144
+ physicalLocation: {
145
+ artifactLocation: {
146
+ uri: normalizeUri(uri),
147
+ },
148
+ region: {
149
+ startLine: parsed?.line ?? 1,
150
+ },
151
+ },
152
+ };
153
+ }
154
+
155
+ function parseEvidenceLocation(evidence) {
156
+ if (!evidence) {
157
+ return null;
158
+ }
159
+
160
+ const ref = evidence.includes(" @ ") ? evidence.split(" @ ").pop() : evidence;
161
+ const match = /^(?<uri>.+?):(?<line>\d+)(?::\d+)?$/.exec(ref);
162
+ if (!match?.groups?.uri) {
163
+ return null;
164
+ }
165
+ return {
166
+ uri: match.groups.uri,
167
+ line: Number(match.groups.line),
168
+ };
169
+ }
170
+
171
+ function junitFindingTestcase(finding) {
172
+ const classname = `plugin-inspector.${xmlName(finding.fixture ?? "unknown")}`;
173
+ const name = `${finding.level ?? "finding"}:${finding.code}`;
174
+ const output = normalizeEvidence(finding.evidence).join("\n");
175
+ if (!isBlockingFinding(finding)) {
176
+ return [
177
+ ` <testcase classname="${escapeXml(classname)}" name="${escapeXml(name)}">`,
178
+ output ? ` <system-out>${escapeXml(output)}</system-out>` : "",
179
+ " </testcase>",
180
+ ]
181
+ .filter(Boolean)
182
+ .join("\n");
183
+ }
184
+
185
+ return [
186
+ ` <testcase classname="${escapeXml(classname)}" name="${escapeXml(name)}">`,
187
+ ` <failure message="${escapeXml(finding.message ?? finding.code)}">${escapeXml(output || finding.message || finding.code)}</failure>`,
188
+ " </testcase>",
189
+ ].join("\n");
190
+ }
191
+
192
+ function junitPassingTestcase(report) {
193
+ return ` <testcase classname="plugin-inspector" name="status:${escapeXml(report.status ?? "pass")}"/>`;
194
+ }
195
+
196
+ function isBlockingFinding(finding) {
197
+ return finding.level === "breakage" || finding.status === "blocking" || finding.severity === "P0";
198
+ }
199
+
200
+ function sarifLevel(finding) {
201
+ if (isBlockingFinding(finding) || finding.severity === "P1") {
202
+ return "error";
203
+ }
204
+ if (finding.level === "warning" || finding.severity === "P2") {
205
+ return "warning";
206
+ }
207
+ return "note";
208
+ }
209
+
210
+ function normalizeEvidence(evidence) {
211
+ if (Array.isArray(evidence)) {
212
+ return evidence.map(String);
213
+ }
214
+ if (evidence == null) {
215
+ return [];
216
+ }
217
+ return [String(evidence)];
218
+ }
219
+
220
+ function normalizeUri(uri) {
221
+ return String(uri).replaceAll(path.sep, "/");
222
+ }
223
+
224
+ function xmlName(value) {
225
+ return String(value).replace(/[^a-zA-Z0-9_.-]/g, "_");
226
+ }
227
+
228
+ function escapeXml(value) {
229
+ return String(value)
230
+ .replace(/&/g, "&amp;")
231
+ .replace(/</g, "&lt;")
232
+ .replace(/>/g, "&gt;")
233
+ .replace(/"/g, "&quot;")
234
+ .replace(/'/g, "&apos;");
235
+ }
package/src/cli.js CHANGED
@@ -1,15 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  import path from "node:path";
3
3
  import {
4
+ loadPluginConfig,
4
5
  renderTextSummary,
5
6
  runPluginCheck,
6
7
  } from "./index.js";
7
8
  import {
8
9
  buildCiSummary,
9
10
  captureEntrypoint,
11
+ defaultJunitPath,
12
+ defaultSarifPath,
10
13
  inspectCompatibilityFixtureSet,
11
14
  inspectFixtureSet,
12
15
  loadInspectorConfig,
16
+ writeCiOutputArtifacts,
13
17
  writeCiSummary,
14
18
  writeCompatibilityReport,
15
19
  writePluginInspectorInit,
@@ -28,8 +32,14 @@ try {
28
32
  await runCheck(commandArgs);
29
33
  } else if (command === "init") {
30
34
  await runInit(commandArgs);
35
+ } else if (command === "config") {
36
+ await runConfig(commandArgs);
31
37
  } else if (command === "inspect" || command === "report") {
32
- await runReport(command, commandArgs);
38
+ if (command === "inspect" && !commandArgs.includes("--config")) {
39
+ await runCheck(commandArgs);
40
+ } else {
41
+ await runReport(command, commandArgs);
42
+ }
33
43
  } else if (command === "ci") {
34
44
  await runCi(commandArgs);
35
45
  } else if (command === "capture") {
@@ -42,6 +52,18 @@ try {
42
52
  process.exitCode = 1;
43
53
  }
44
54
 
55
+ async function runConfig(commandArgs) {
56
+ const configPath = readFlag(commandArgs, "--config");
57
+ const pluginRoot = readFlag(commandArgs, "--plugin-root") ?? readFlag(commandArgs, "--root");
58
+ const config = await loadPluginConfig({ configPath, pluginRoot });
59
+
60
+ if (commandArgs.includes("--json")) {
61
+ console.log(JSON.stringify(config, null, 2));
62
+ } else {
63
+ console.log(renderConfigTextSummary(config));
64
+ }
65
+ }
66
+
45
67
  async function runCheck(commandArgs) {
46
68
  const configPath = readFlag(commandArgs, "--config");
47
69
  const pluginRoot = readFlag(commandArgs, "--plugin-root") ?? readFlag(commandArgs, "--root");
@@ -50,12 +72,27 @@ async function runCheck(commandArgs) {
50
72
  const json = commandArgs.includes("--json");
51
73
  const capture = readRuntimeFlag(commandArgs);
52
74
  const mockSdk = readMockSdkFlag(commandArgs);
53
- const { report } = await runPluginCheck({ configPath, pluginRoot, outDir, openclawPath, capture, mockSdk });
75
+ const allowExecution = readAllowExecutionFlag(commandArgs);
76
+ const ciOutputs = readCiOutputFlags(commandArgs);
77
+ const { report, paths } = await runPluginCheck({
78
+ allowExecution,
79
+ capture,
80
+ configPath,
81
+ mockSdk,
82
+ openclawPath,
83
+ outDir,
84
+ pluginRoot,
85
+ });
86
+ await writeCiOutputArtifacts(report, {
87
+ ...ciOutputs,
88
+ cwd: path.dirname(paths.jsonPath),
89
+ outDir: ".",
90
+ });
54
91
 
55
92
  if (json) {
56
93
  console.log(JSON.stringify(report, null, 2));
57
94
  } else {
58
- console.log(renderTextSummary(report));
95
+ console.log(renderTextSummary(report, { artifacts: paths }));
59
96
  }
60
97
 
61
98
  if (report.status !== "pass") {
@@ -67,19 +104,27 @@ async function runInit(commandArgs) {
67
104
  const pluginRoot = readFlag(commandArgs, "--plugin-root") ?? readFlag(commandArgs, "--root");
68
105
  const configPath = readFlag(commandArgs, "--config") ?? undefined;
69
106
  const workflowPath = readFlag(commandArgs, "--workflow") ?? undefined;
70
- const packageManager = readFlag(commandArgs, "--package-manager") ?? "npm";
107
+ const packageManager = readFlag(commandArgs, "--package-manager") ?? undefined;
71
108
  const result = await writePluginInspectorInit({
72
109
  pluginRoot,
73
110
  configPath,
74
111
  workflowPath,
75
112
  packageManager,
76
113
  ci: commandArgs.includes("--ci"),
114
+ dryRun: commandArgs.includes("--dry-run"),
115
+ scripts: commandArgs.includes("--scripts"),
77
116
  force: commandArgs.includes("--force"),
78
117
  });
79
118
 
119
+ if (commandArgs.includes("--json")) {
120
+ console.log(JSON.stringify(initCommandSummary(result), null, 2));
121
+ return;
122
+ }
123
+
80
124
  for (const filePath of result.written) {
81
- console.log(`wrote ${filePath}`);
125
+ console.log(`${result.dryRun ? "would write" : "wrote"} ${path.relative(result.pluginRoot, filePath)}`);
82
126
  }
127
+ console.log(`package manager: ${result.packageManager}`);
83
128
  }
84
129
 
85
130
  async function runReport(command, commandArgs) {
@@ -87,14 +132,20 @@ async function runReport(command, commandArgs) {
87
132
  const outDir = readFlag(commandArgs, "--out") ?? "reports";
88
133
  const check = commandArgs.includes("--check") || command === "ci";
89
134
  const json = commandArgs.includes("--json");
135
+ const ciOutputs = readCiOutputFlags(commandArgs);
90
136
  const config = await loadInspectorConfig(configPath);
91
137
  const report = await inspectFixtureSet(config);
92
- await writeReport(report, { outDir });
138
+ const paths = await writeReport(report, { outDir });
139
+ await writeCiOutputArtifacts(report, {
140
+ ...ciOutputs,
141
+ cwd: path.dirname(paths.jsonPath),
142
+ outDir: ".",
143
+ });
93
144
 
94
145
  if (json) {
95
146
  console.log(JSON.stringify(report, null, 2));
96
147
  } else {
97
- console.log(renderTextSummary(report));
148
+ console.log(renderTextSummary(report, { artifacts: paths }));
98
149
  }
99
150
 
100
151
  if (check && report.status !== "pass") {
@@ -108,8 +159,15 @@ async function runCi(commandArgs) {
108
159
  const outDir = readFlag(commandArgs, "--out") ?? "reports";
109
160
  const openclawPath = commandArgs.includes("--no-openclaw") ? false : readFlag(commandArgs, "--openclaw");
110
161
  const json = commandArgs.includes("--json");
162
+ const capture = readRuntimeFlag(commandArgs);
163
+ const mockSdk = readMockSdkFlag(commandArgs);
164
+ const allowExecution = readAllowExecutionFlag(commandArgs);
165
+ const ciOutputs = readCiOutputFlags(commandArgs, { defaultEnabled: true });
111
166
  const { report, reportDir } = await runCiCompatibilityReport({
167
+ allowExecution,
168
+ capture,
112
169
  configPath,
170
+ mockSdk,
113
171
  openclawPath,
114
172
  outDir,
115
173
  pluginRoot,
@@ -128,6 +186,11 @@ async function runCi(commandArgs) {
128
186
  jsonPath: path.join(reportDir, "plugin-inspector-ci-summary.json"),
129
187
  markdownPath: path.join(reportDir, "plugin-inspector-ci-summary.md"),
130
188
  });
189
+ await writeCiOutputArtifacts(report, {
190
+ ...ciOutputs,
191
+ cwd: reportDir,
192
+ outDir: ".",
193
+ });
131
194
 
132
195
  if (json) {
133
196
  console.log(JSON.stringify(summary, null, 2));
@@ -140,7 +203,7 @@ async function runCi(commandArgs) {
140
203
  }
141
204
  }
142
205
 
143
- async function runCiCompatibilityReport({ configPath, openclawPath, outDir, pluginRoot }) {
206
+ async function runCiCompatibilityReport({ allowExecution, capture, configPath, mockSdk, openclawPath, outDir, pluginRoot }) {
144
207
  if (configPath) {
145
208
  const config = await loadInspectorConfig(configPath, { cwd: pluginRoot });
146
209
  const report = await inspectCompatibilityFixtureSet(config, { openclawPath });
@@ -151,7 +214,7 @@ async function runCiCompatibilityReport({ configPath, openclawPath, outDir, plug
151
214
  };
152
215
  }
153
216
 
154
- const { report } = await runPluginCheck({ pluginRoot, outDir, openclawPath });
217
+ const { report } = await runPluginCheck({ allowExecution, capture, mockSdk, openclawPath, outDir, pluginRoot });
155
218
  return {
156
219
  report,
157
220
  reportDir: path.resolve(pluginRoot ?? process.cwd(), outDir),
@@ -163,11 +226,12 @@ async function runCapture(commandArgs) {
163
226
  const outputPath = readFlag(commandArgs, "--output");
164
227
  const pluginRoot = readFlag(commandArgs, "--plugin-root");
165
228
  const mockSdk = readMockSdkFlag(commandArgs) ?? commandArgs.includes("--mock-sdk");
229
+ const allowExecution = readAllowExecutionFlag(commandArgs);
166
230
  if (!entrypoint) {
167
231
  throw new Error("capture requires an entrypoint path");
168
232
  }
169
- if (process.env.PLUGIN_INSPECTOR_EXECUTE_ISOLATED !== "1") {
170
- throw new Error("capture imports plugin code; rerun with PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 in an isolated workspace");
233
+ if (!allowExecution && process.env.PLUGIN_INSPECTOR_EXECUTE_ISOLATED !== "1") {
234
+ throw new Error("capture imports plugin code; rerun with PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 or --allow-execute in an isolated workspace");
171
235
  }
172
236
 
173
237
  const result = await captureEntrypoint(entrypoint, { mockSdk, pluginRoot });
@@ -187,6 +251,26 @@ function readFlag(commandArgs, name) {
187
251
  return commandArgs[index + 1] ?? null;
188
252
  }
189
253
 
254
+ function readOptionalPathFlag(commandArgs, name, defaultPath) {
255
+ const index = commandArgs.indexOf(name);
256
+ if (index === -1) {
257
+ return null;
258
+ }
259
+ const value = commandArgs[index + 1];
260
+ return value && !value.startsWith("-") ? value : defaultPath;
261
+ }
262
+
263
+ function readCiOutputFlags(commandArgs, options = {}) {
264
+ return {
265
+ sarifPath: commandArgs.includes("--no-sarif")
266
+ ? null
267
+ : (readOptionalPathFlag(commandArgs, "--sarif", defaultSarifPath) ?? (options.defaultEnabled ? defaultSarifPath : null)),
268
+ junitPath: commandArgs.includes("--no-junit")
269
+ ? null
270
+ : (readOptionalPathFlag(commandArgs, "--junit", defaultJunitPath) ?? (options.defaultEnabled ? defaultJunitPath : null)),
271
+ };
272
+ }
273
+
190
274
  function readRuntimeFlag(commandArgs) {
191
275
  if (commandArgs.includes("--runtime") || commandArgs.includes("--capture")) {
192
276
  return true;
@@ -217,6 +301,10 @@ function readMockSdkFlag(commandArgs) {
217
301
  return undefined;
218
302
  }
219
303
 
304
+ function readAllowExecutionFlag(commandArgs) {
305
+ return commandArgs.includes("--allow-execute");
306
+ }
307
+
220
308
  function renderCiTextSummary(summary) {
221
309
  return [
222
310
  `Status: ${summary.status.toUpperCase()}`,
@@ -226,19 +314,43 @@ function renderCiTextSummary(summary) {
226
314
  ].join("\n");
227
315
  }
228
316
 
317
+ function initCommandSummary(result) {
318
+ return {
319
+ dryRun: result.dryRun,
320
+ packageManager: result.packageManager,
321
+ pluginRoot: result.pluginRoot,
322
+ files: result.written.map((filePath) => path.relative(result.pluginRoot, filePath)),
323
+ };
324
+ }
325
+
326
+ function renderConfigTextSummary(config) {
327
+ const fixture = config.fixtures[0];
328
+ return [
329
+ `Plugin: ${fixture.id}`,
330
+ `Root: ${config.rootDir}`,
331
+ `Config: ${config.configPath ?? "auto"}`,
332
+ `Priority: ${fixture.priority}`,
333
+ `Seams: ${fixture.seams.join(", ")}`,
334
+ `Runtime capture: ${config.capture?.runtime === true ? "on" : "off"}`,
335
+ `Mock SDK: ${config.capture?.mockSdk === false ? "off" : "on"}`,
336
+ ].join("\n");
337
+ }
338
+
229
339
  function printHelp() {
230
340
  console.log(`plugin-inspector
231
341
 
232
342
  Usage:
233
343
  plugin-inspector
234
- plugin-inspector check [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--runtime] [--mock-sdk|--real-sdk] [--json]
235
- plugin-inspector init [--plugin-root <path>] [--config <path>] [--ci] [--package-manager npm|pnpm|yarn|bun] [--force]
344
+ plugin-inspector check [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--runtime] [--mock-sdk|--real-sdk] [--allow-execute] [--json]
345
+ plugin-inspector config [--plugin-root <path>] [--config <path>] [--json]
346
+ plugin-inspector init [--plugin-root <path>] [--config <path>] [--ci] [--scripts] [--package-manager npm|pnpm|yarn|bun] [--dry-run] [--json] [--force]
236
347
  plugin-inspector report --config <path> [--out <dir>] [--check] [--json]
237
- plugin-inspector inspect --config <path> [--out <dir>] [--check] [--json]
238
- plugin-inspector ci [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--json]
239
- PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 plugin-inspector capture <entrypoint> [--mock-sdk|--real-sdk] [--plugin-root <path>] [--output <path>]
348
+ plugin-inspector inspect [--plugin-root <path>] [--config <path>] [--out <dir>] [--check] [--json] [--sarif [path]] [--junit [path]] [--allow-execute]
349
+ 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]
350
+ plugin-inspector capture <entrypoint> [--mock-sdk|--real-sdk] [--allow-execute] [--plugin-root <path>] [--output <path>]
240
351
 
241
352
  Default check runs from the current plugin root and writes reports/ unless --out is set.
242
- Runtime capture is opt-in because it imports plugin code; use --runtime with PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1.
353
+ CI writes SARIF and JUnit artifacts by default; check/inspect can write them with --sarif and --junit.
354
+ Runtime capture is opt-in because it imports plugin code; use --runtime with --allow-execute or PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1.
243
355
  `);
244
356
  }
package/src/config.js CHANGED
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
 
5
5
  export const npmPackagePayloadDir = ".crabpot-package";
6
6
  export const defaultPluginRootConfigFiles = ["plugin-inspector.config.json", ".plugin-inspector.json"];
7
+ export const packageJsonConfigKeys = ["pluginInspector", "plugin-inspector"];
7
8
 
8
9
  export async function loadInspectorConfig(configPath, options = {}) {
9
10
  if (!configPath) {
@@ -24,16 +25,23 @@ export async function loadInspectorConfig(configPath, options = {}) {
24
25
  export async function loadPluginRootConfig(configPath = null, options = {}) {
25
26
  const rootDir = path.resolve(options.cwd ?? process.cwd());
26
27
  const resolvedPath = configPath ? path.resolve(rootDir, configPath) : findPluginRootConfigPath(rootDir);
27
- if (!resolvedPath && !existsSync(path.join(rootDir, "package.json")) && !existsSync(path.join(rootDir, "openclaw.plugin.json"))) {
28
+ const packageJsonPath = path.join(rootDir, "package.json");
29
+ const packageJson = await readJsonIfExists(packageJsonPath);
30
+ const packageConfig = packageJsonConfig(packageJson);
31
+
32
+ if (!resolvedPath && !packageJson && !existsSync(path.join(rootDir, "openclaw.plugin.json"))) {
28
33
  throw new Error("run from a plugin root with package.json/openclaw.plugin.json, or pass --config");
29
34
  }
30
- const config = resolvedPath ? JSON.parse(await readFile(resolvedPath, "utf8")) : { version: 1 };
35
+
36
+ const config = resolvedPath
37
+ ? JSON.parse(await readFile(resolvedPath, "utf8"))
38
+ : (packageConfig.config ?? { version: 1 });
31
39
  const normalizedConfig = await normalizePluginRootConfig(config, { rootDir });
32
40
  validateInspectorConfig(normalizedConfig);
33
41
  return {
34
42
  ...normalizedConfig,
35
43
  rootDir,
36
- configPath: resolvedPath,
44
+ configPath: resolvedPath ?? packageConfig.configPath,
37
45
  };
38
46
  }
39
47
 
@@ -164,6 +172,25 @@ function findPluginRootConfigPath(rootDir) {
164
172
  return defaultPluginRootConfigFiles.map((file) => path.join(rootDir, file)).find(existsSync) ?? null;
165
173
  }
166
174
 
175
+ function packageJsonConfig(packageJson) {
176
+ if (!packageJson) {
177
+ return { config: null, configPath: null };
178
+ }
179
+ for (const key of packageJsonConfigKeys) {
180
+ if (packageJson[key] === undefined) {
181
+ continue;
182
+ }
183
+ if (!packageJson[key] || typeof packageJson[key] !== "object" || Array.isArray(packageJson[key])) {
184
+ throw new Error(`package.json ${key} must be an object`);
185
+ }
186
+ return {
187
+ config: packageJson[key],
188
+ configPath: `package.json#${key}`,
189
+ };
190
+ }
191
+ return { config: null, configPath: null };
192
+ }
193
+
167
194
  async function readJsonIfExists(filePath) {
168
195
  if (!existsSync(filePath)) {
169
196
  return null;
package/src/index.js CHANGED
@@ -1,11 +1,17 @@
1
1
  export {
2
2
  capturePluginEntrypoint,
3
3
  createCaptureApi,
4
+ inspectCompatibilityFixtureSetConfig,
4
5
  inspectFixtureSetConfig,
5
6
  inspectPluginRoot,
6
7
  loadPluginConfig,
8
+ renderFixtureSetIssuesReport,
9
+ renderFixtureSetMarkdownReport,
7
10
  renderTextSummary,
11
+ runFixtureSetReport,
8
12
  runPluginCheck,
9
13
  setupPluginInspector,
14
+ writeCiOutputArtifacts,
15
+ writeFixtureSetReports,
10
16
  writePluginReports,
11
17
  } from "./api.js";