@openclaw/plugin-inspector 0.2.0 → 0.3.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/src/cli.js CHANGED
@@ -72,8 +72,17 @@ async function runCheck(commandArgs) {
72
72
  const json = commandArgs.includes("--json");
73
73
  const capture = readRuntimeFlag(commandArgs);
74
74
  const mockSdk = readMockSdkFlag(commandArgs);
75
+ const allowExecution = readAllowExecutionFlag(commandArgs);
75
76
  const ciOutputs = readCiOutputFlags(commandArgs);
76
- const { report, paths } = await runPluginCheck({ configPath, pluginRoot, outDir, openclawPath, capture, mockSdk });
77
+ const { report, paths } = await runPluginCheck({
78
+ allowExecution,
79
+ capture,
80
+ configPath,
81
+ mockSdk,
82
+ openclawPath,
83
+ outDir,
84
+ pluginRoot,
85
+ });
77
86
  await writeCiOutputArtifacts(report, {
78
87
  ...ciOutputs,
79
88
  cwd: path.dirname(paths.jsonPath),
@@ -83,7 +92,7 @@ async function runCheck(commandArgs) {
83
92
  if (json) {
84
93
  console.log(JSON.stringify(report, null, 2));
85
94
  } else {
86
- console.log(renderTextSummary(report));
95
+ console.log(renderTextSummary(report, { artifacts: paths }));
87
96
  }
88
97
 
89
98
  if (report.status !== "pass") {
@@ -95,19 +104,27 @@ async function runInit(commandArgs) {
95
104
  const pluginRoot = readFlag(commandArgs, "--plugin-root") ?? readFlag(commandArgs, "--root");
96
105
  const configPath = readFlag(commandArgs, "--config") ?? undefined;
97
106
  const workflowPath = readFlag(commandArgs, "--workflow") ?? undefined;
98
- const packageManager = readFlag(commandArgs, "--package-manager") ?? "npm";
107
+ const packageManager = readFlag(commandArgs, "--package-manager") ?? undefined;
99
108
  const result = await writePluginInspectorInit({
100
109
  pluginRoot,
101
110
  configPath,
102
111
  workflowPath,
103
112
  packageManager,
104
113
  ci: commandArgs.includes("--ci"),
114
+ dryRun: commandArgs.includes("--dry-run"),
115
+ scripts: commandArgs.includes("--scripts"),
105
116
  force: commandArgs.includes("--force"),
106
117
  });
107
118
 
119
+ if (commandArgs.includes("--json")) {
120
+ console.log(JSON.stringify(initCommandSummary(result), null, 2));
121
+ return;
122
+ }
123
+
108
124
  for (const filePath of result.written) {
109
- console.log(`wrote ${filePath}`);
125
+ console.log(`${result.dryRun ? "would write" : "wrote"} ${path.relative(result.pluginRoot, filePath)}`);
110
126
  }
127
+ console.log(`package manager: ${result.packageManager}`);
111
128
  }
112
129
 
113
130
  async function runReport(command, commandArgs) {
@@ -128,7 +145,7 @@ async function runReport(command, commandArgs) {
128
145
  if (json) {
129
146
  console.log(JSON.stringify(report, null, 2));
130
147
  } else {
131
- console.log(renderTextSummary(report));
148
+ console.log(renderTextSummary(report, { artifacts: paths }));
132
149
  }
133
150
 
134
151
  if (check && report.status !== "pass") {
@@ -144,8 +161,10 @@ async function runCi(commandArgs) {
144
161
  const json = commandArgs.includes("--json");
145
162
  const capture = readRuntimeFlag(commandArgs);
146
163
  const mockSdk = readMockSdkFlag(commandArgs);
164
+ const allowExecution = readAllowExecutionFlag(commandArgs);
147
165
  const ciOutputs = readCiOutputFlags(commandArgs, { defaultEnabled: true });
148
166
  const { report, reportDir } = await runCiCompatibilityReport({
167
+ allowExecution,
149
168
  capture,
150
169
  configPath,
151
170
  mockSdk,
@@ -184,7 +203,7 @@ async function runCi(commandArgs) {
184
203
  }
185
204
  }
186
205
 
187
- async function runCiCompatibilityReport({ capture, configPath, mockSdk, openclawPath, outDir, pluginRoot }) {
206
+ async function runCiCompatibilityReport({ allowExecution, capture, configPath, mockSdk, openclawPath, outDir, pluginRoot }) {
188
207
  if (configPath) {
189
208
  const config = await loadInspectorConfig(configPath, { cwd: pluginRoot });
190
209
  const report = await inspectCompatibilityFixtureSet(config, { openclawPath });
@@ -195,7 +214,7 @@ async function runCiCompatibilityReport({ capture, configPath, mockSdk, openclaw
195
214
  };
196
215
  }
197
216
 
198
- const { report } = await runPluginCheck({ pluginRoot, outDir, openclawPath, capture, mockSdk });
217
+ const { report } = await runPluginCheck({ allowExecution, capture, mockSdk, openclawPath, outDir, pluginRoot });
199
218
  return {
200
219
  report,
201
220
  reportDir: path.resolve(pluginRoot ?? process.cwd(), outDir),
@@ -207,11 +226,12 @@ async function runCapture(commandArgs) {
207
226
  const outputPath = readFlag(commandArgs, "--output");
208
227
  const pluginRoot = readFlag(commandArgs, "--plugin-root");
209
228
  const mockSdk = readMockSdkFlag(commandArgs) ?? commandArgs.includes("--mock-sdk");
229
+ const allowExecution = readAllowExecutionFlag(commandArgs);
210
230
  if (!entrypoint) {
211
231
  throw new Error("capture requires an entrypoint path");
212
232
  }
213
- if (process.env.PLUGIN_INSPECTOR_EXECUTE_ISOLATED !== "1") {
214
- 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");
215
235
  }
216
236
 
217
237
  const result = await captureEntrypoint(entrypoint, { mockSdk, pluginRoot });
@@ -281,6 +301,10 @@ function readMockSdkFlag(commandArgs) {
281
301
  return undefined;
282
302
  }
283
303
 
304
+ function readAllowExecutionFlag(commandArgs) {
305
+ return commandArgs.includes("--allow-execute");
306
+ }
307
+
284
308
  function renderCiTextSummary(summary) {
285
309
  return [
286
310
  `Status: ${summary.status.toUpperCase()}`,
@@ -290,6 +314,15 @@ function renderCiTextSummary(summary) {
290
314
  ].join("\n");
291
315
  }
292
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
+
293
326
  function renderConfigTextSummary(config) {
294
327
  const fixture = config.fixtures[0];
295
328
  return [
@@ -308,16 +341,16 @@ function printHelp() {
308
341
 
309
342
  Usage:
310
343
  plugin-inspector
311
- plugin-inspector check [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--runtime] [--mock-sdk|--real-sdk] [--json]
344
+ plugin-inspector check [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--runtime] [--mock-sdk|--real-sdk] [--allow-execute] [--json]
312
345
  plugin-inspector config [--plugin-root <path>] [--config <path>] [--json]
313
- plugin-inspector init [--plugin-root <path>] [--config <path>] [--ci] [--package-manager npm|pnpm|yarn|bun] [--force]
346
+ plugin-inspector init [--plugin-root <path>] [--config <path>] [--ci] [--scripts] [--package-manager npm|pnpm|yarn|bun] [--dry-run] [--json] [--force]
314
347
  plugin-inspector report --config <path> [--out <dir>] [--check] [--json]
315
- plugin-inspector inspect [--plugin-root <path>] [--config <path>] [--out <dir>] [--check] [--json] [--sarif [path]] [--junit [path]]
316
- plugin-inspector ci [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--runtime] [--mock-sdk|--real-sdk] [--json] [--no-sarif] [--no-junit]
317
- 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>]
318
351
 
319
352
  Default check runs from the current plugin root and writes reports/ unless --out is set.
320
353
  CI writes SARIF and JUnit artifacts by default; check/inspect can write them with --sarif and --junit.
321
- Runtime capture is opt-in because it imports plugin code; use --runtime with PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1.
354
+ Runtime capture is opt-in because it imports plugin code; use --runtime with --allow-execute or PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1.
322
355
  `);
323
356
  }
package/src/index.js CHANGED
@@ -1,12 +1,239 @@
1
+ import * as pluginApi from "./api.js";
2
+ import * as ciPolicyApi from "./ci-policy.js";
3
+ import * as ciSummaryApi from "./ci-summary.js";
4
+ import * as configApi from "./config.js";
5
+ import * as contractCaptureApi from "./contract-capture.js";
6
+ import * as contractCoverageApi from "./contract-coverage.js";
7
+ import * as executionResultsApi from "./execution-results.js";
8
+ import * as importLoopProfileApi from "./import-loop-profile.js";
9
+ import * as inspectorApi from "./inspector.js";
10
+ import * as issuesApi from "./issues.js";
11
+ import * as openClawTargetApi from "./openclaw-target.js";
12
+ import * as profileDiffApi from "./profile-diff.js";
13
+ import * as refDiffApi from "./ref-diff.js";
14
+ import * as reportApi from "./report.js";
15
+ import * as runtimeProfileApi from "./runtime-profile.js";
16
+ import * as syntheticProbeSuiteApi from "./synthetic-probe-suite.js";
17
+ import * as syntheticProbesApi from "./synthetic-probes.js";
18
+
19
+ export const pluginRoot = Object.freeze({
20
+ loadConfig: pluginApi.loadPluginConfig,
21
+ inspect: pluginApi.inspectPluginRoot,
22
+ runCheck: pluginApi.runPluginCheck,
23
+ captureEntrypoint: pluginApi.capturePluginEntrypoint,
24
+ setup: pluginApi.setupPluginInspector,
25
+ });
26
+
27
+ export const fixtureSuites = Object.freeze({
28
+ loadConfig: configApi.loadInspectorConfig,
29
+ inspect: pluginApi.inspectCompatibilityFixtureSetConfig,
30
+ inspectStatic: pluginApi.inspectFixtureSetConfig,
31
+ runReport: pluginApi.runFixtureSetReport,
32
+ writeReports: pluginApi.writeFixtureSetReports,
33
+ renderReport: pluginApi.renderFixtureSetMarkdownReport,
34
+ renderIssues: pluginApi.renderFixtureSetIssuesReport,
35
+ buildColdImportReadiness: pluginApi.buildFixtureSetColdImportReadiness,
36
+ runColdImportReadiness: pluginApi.runFixtureSetColdImportReadiness,
37
+ writeColdImportReadiness: pluginApi.writeFixtureSetColdImportReadiness,
38
+ buildWorkspacePlan: pluginApi.buildFixtureSetWorkspacePlan,
39
+ runWorkspacePlan: pluginApi.runFixtureSetWorkspacePlan,
40
+ writeWorkspacePlan: pluginApi.writeFixtureSetWorkspacePlan,
41
+ buildPlatformProbes: pluginApi.buildFixtureSetPlatformProbes,
42
+ runPlatformProbes: pluginApi.runFixtureSetPlatformProbes,
43
+ writePlatformProbes: pluginApi.writeFixtureSetPlatformProbes,
44
+ });
45
+
46
+ export const staticInspection = Object.freeze({
47
+ loadConfig: configApi.loadInspectorConfig,
48
+ inspectSourceText: inspectorApi.inspectSourceText,
49
+ inspectPlugin: inspectorApi.inspectPlugin,
50
+ inspectFixtureSet: inspectorApi.inspectFixtureSet,
51
+ });
52
+
53
+ export const reports = Object.freeze({
54
+ renderMarkdown: reportApi.renderMarkdownReport,
55
+ renderTextSummary: pluginApi.renderTextSummary,
56
+ write: reportApi.writeReport,
57
+ issueId: issuesApi.issueId,
58
+ classifyIssueFinding: issuesApi.classifyIssueFinding,
59
+ knownIssueCodes: issuesApi.knownIssueCodes,
60
+ openClawTargetPathCandidates: openClawTargetApi.openClawTargetPathCandidates,
61
+ });
62
+
63
+ export const contracts = Object.freeze({
64
+ buildCapture: contractCaptureApi.buildContractCapture,
65
+ writeCapture: contractCaptureApi.writeContractCapture,
66
+ renderCapture: contractCaptureApi.renderContractCaptureMarkdown,
67
+ validateCapture: contractCaptureApi.validateContractCapture,
68
+ validateCoverage: contractCoverageApi.validateContractCoverage,
69
+ defaults: Object.freeze({
70
+ registrationAssertions: contractCaptureApi.defaultRegistrationAssertions,
71
+ registrationArguments: contractCaptureApi.defaultRegistrationArguments,
72
+ hookAssertions: contractCaptureApi.defaultHookAssertions,
73
+ hookEvents: contractCaptureApi.defaultHookEvents,
74
+ hookContexts: contractCaptureApi.defaultHookContexts,
75
+ }),
76
+ });
77
+
78
+ export const ci = Object.freeze({
79
+ buildSummary: ciSummaryApi.buildCiSummary,
80
+ writeSummary: ciSummaryApi.writeCiSummary,
81
+ renderSummary: ciSummaryApi.renderCiSummaryMarkdown,
82
+ readReports: ciSummaryApi.readCiReports,
83
+ deriveStatus: ciSummaryApi.deriveCiStatus,
84
+ buildPolicyReport: ciPolicyApi.buildCiPolicyReport,
85
+ writePolicyReport: ciPolicyApi.writeCiPolicyReport,
86
+ renderPolicyReport: ciPolicyApi.renderCiPolicyMarkdown,
87
+ validatePolicy: ciPolicyApi.validateCiPolicy,
88
+ validatePolicyReport: ciPolicyApi.validateCiPolicyReport,
89
+ buildExecutionResults: executionResultsApi.buildExecutionResultsReport,
90
+ writeExecutionResults: executionResultsApi.writeExecutionResultsReport,
91
+ renderExecutionResults: executionResultsApi.renderExecutionResultsMarkdown,
92
+ writeOutputs: pluginApi.writeCiOutputArtifacts,
93
+ });
94
+
95
+ export const runtime = Object.freeze({
96
+ buildProfile: runtimeProfileApi.buildRuntimeProfile,
97
+ writeProfile: runtimeProfileApi.writeRuntimeProfile,
98
+ renderProfile: runtimeProfileApi.renderRuntimeProfileMarkdown,
99
+ validateProfile: runtimeProfileApi.validateRuntimeProfile,
100
+ buildProfileDiff: profileDiffApi.buildProfileDiff,
101
+ writeProfileDiff: profileDiffApi.writeProfileDiff,
102
+ renderProfileDiff: profileDiffApi.renderProfileDiffMarkdown,
103
+ validateProfileDiff: profileDiffApi.validateProfileDiff,
104
+ buildRefDiff: refDiffApi.buildRefDiff,
105
+ writeRefDiff: refDiffApi.writeRefDiff,
106
+ renderRefDiff: refDiffApi.renderRefDiffMarkdown,
107
+ validateRefDiff: refDiffApi.validateRefDiff,
108
+ buildImportLoopProfile: importLoopProfileApi.buildImportLoopProfile,
109
+ writeImportLoopProfile: importLoopProfileApi.writeImportLoopProfile,
110
+ renderImportLoopProfile: importLoopProfileApi.renderImportLoopProfileMarkdown,
111
+ validateImportLoopProfile: importLoopProfileApi.validateImportLoopProfile,
112
+ });
113
+
114
+ export const synthetic = Object.freeze({
115
+ buildPlan: syntheticProbesApi.buildSyntheticProbePlan,
116
+ buildPlanFromReport: syntheticProbeSuiteApi.buildSyntheticProbePlanFromReport,
117
+ writePlan: syntheticProbesApi.writeSyntheticProbePlan,
118
+ renderPlan: syntheticProbesApi.renderSyntheticProbeMarkdown,
119
+ validatePlan: syntheticProbesApi.validateSyntheticProbePlan,
120
+ runCaptured: syntheticProbesApi.runCapturedSyntheticProbes,
121
+ registrationExecutionProfiles: syntheticProbesApi.syntheticRegistrationExecutionProfiles,
122
+ defaultHookEvents: syntheticProbesApi.defaultSyntheticHookEvents,
123
+ defaultHookContexts: syntheticProbesApi.defaultSyntheticHookContexts,
124
+ defaultRegistrationArguments: syntheticProbesApi.defaultSyntheticRegistrationArguments,
125
+ });
126
+
1
127
  export {
2
128
  capturePluginEntrypoint,
129
+ buildFixtureSetColdImportReadiness,
130
+ buildFixtureSetPlatformProbes,
131
+ buildFixtureSetWorkspacePlan,
3
132
  createCaptureApi,
133
+ inspectCompatibilityFixtureSetConfig,
4
134
  inspectFixtureSetConfig,
5
135
  inspectPluginRoot,
6
136
  loadPluginConfig,
137
+ renderFixtureSetColdImportReadinessMarkdown,
138
+ renderFixtureSetIssuesReport,
139
+ renderFixtureSetMarkdownReport,
140
+ renderFixtureSetPlatformProbesMarkdown,
141
+ renderFixtureSetWorkspacePlanMarkdown,
7
142
  renderTextSummary,
143
+ runFixtureSetColdImportReadiness,
144
+ runFixtureSetPlatformProbes,
145
+ runFixtureSetReport,
146
+ runFixtureSetWorkspacePlan,
8
147
  runPluginCheck,
9
148
  setupPluginInspector,
149
+ validateColdImportReadiness,
150
+ validateFixtureSetPlatformProbes,
151
+ validateFixtureSetWorkspacePlan,
10
152
  writeCiOutputArtifacts,
153
+ writeFixtureSetColdImportReadiness,
154
+ writeFixtureSetPlatformProbes,
155
+ writeFixtureSetReports,
156
+ writeFixtureSetWorkspacePlan,
11
157
  writePluginReports,
12
158
  } from "./api.js";
159
+ export {
160
+ buildContractCapture,
161
+ defaultHookAssertions,
162
+ defaultHookContexts,
163
+ defaultHookEvents,
164
+ defaultRegistrationArguments,
165
+ defaultRegistrationAssertions,
166
+ renderContractCaptureMarkdown,
167
+ validateContractCapture,
168
+ writeContractCapture,
169
+ } from "./contract-capture.js";
170
+ export { validateContractCoverage } from "./contract-coverage.js";
171
+ export {
172
+ buildCiPolicyReport,
173
+ defaultCiPolicyReportOptions,
174
+ renderCiPolicyMarkdown,
175
+ validateCiPolicy,
176
+ validateCiPolicyReport,
177
+ writeCiPolicyReport,
178
+ } from "./ci-policy.js";
179
+ export {
180
+ buildCiSummary,
181
+ defaultCiReportPaths,
182
+ deriveCiStatus,
183
+ readCiReports,
184
+ renderCiSummaryMarkdown,
185
+ writeCiSummary,
186
+ } from "./ci-summary.js";
187
+ export { loadInspectorConfig } from "./config.js";
188
+ export {
189
+ buildExecutionResultsReport,
190
+ defaultExecutionResultsOptions,
191
+ renderExecutionResultsMarkdown,
192
+ writeExecutionResultsReport,
193
+ } from "./execution-results.js";
194
+ export {
195
+ buildImportLoopProfile,
196
+ defaultImportLoopProfileOptions,
197
+ renderImportLoopProfileMarkdown,
198
+ validateImportLoopProfile,
199
+ writeImportLoopProfile,
200
+ } from "./import-loop-profile.js";
201
+ export { classifyIssueFinding, issueId, knownIssueCodes } from "./issues.js";
202
+ export { inspectFixtureSet, inspectPlugin, inspectSourceText } from "./inspector.js";
203
+ export { openClawTargetPathCandidates } from "./openclaw-target.js";
204
+ export {
205
+ buildProfileDiff,
206
+ defaultProfileDiffOptions,
207
+ renderProfileDiffMarkdown,
208
+ validateProfileDiff,
209
+ writeProfileDiff,
210
+ } from "./profile-diff.js";
211
+ export {
212
+ buildRefDiff,
213
+ defaultRefDiffDimensions,
214
+ defaultRefDiffOptions,
215
+ renderRefDiffMarkdown,
216
+ validateRefDiff,
217
+ writeRefDiff,
218
+ } from "./ref-diff.js";
219
+ export { renderMarkdownReport, writeReport } from "./report.js";
220
+ export {
221
+ buildRuntimeProfile,
222
+ defaultRuntimeProfileCommands,
223
+ defaultRuntimeProfileOptions,
224
+ renderRuntimeProfileMarkdown,
225
+ validateRuntimeProfile,
226
+ writeRuntimeProfile,
227
+ } from "./runtime-profile.js";
228
+ export { buildSyntheticProbePlanFromReport } from "./synthetic-probe-suite.js";
229
+ export {
230
+ buildSyntheticProbePlan,
231
+ defaultSyntheticHookContexts,
232
+ defaultSyntheticHookEvents,
233
+ defaultSyntheticRegistrationArguments,
234
+ renderSyntheticProbeMarkdown,
235
+ runCapturedSyntheticProbes,
236
+ syntheticRegistrationExecutionProfiles,
237
+ validateSyntheticProbePlan,
238
+ writeSyntheticProbePlan,
239
+ } from "./synthetic-probes.js";
package/src/init.js CHANGED
@@ -5,32 +5,66 @@ import { inferPluginSeams, packageId } from "./config.js";
5
5
 
6
6
  export const defaultInitConfigPath = "plugin-inspector.config.json";
7
7
  export const defaultInitWorkflowPath = ".github/workflows/plugin-inspector.yml";
8
+ export const defaultInitPackageScripts = {
9
+ "plugin:check": "plugin-inspector inspect --no-openclaw",
10
+ "plugin:ci": "plugin-inspector ci --no-openclaw --runtime --mock-sdk --allow-execute",
11
+ };
8
12
 
9
13
  export async function writePluginInspectorInit(options = {}) {
10
14
  const pluginRoot = path.resolve(options.pluginRoot ?? options.cwd ?? process.cwd());
11
15
  const configPath = path.resolve(pluginRoot, options.configPath ?? defaultInitConfigPath);
16
+ const workflowPath = options.ci === true ? path.resolve(pluginRoot, options.workflowPath ?? defaultInitWorkflowPath) : null;
17
+ const packageManager = options.packageManager ?? (await detectPackageManager(pluginRoot));
18
+ const dryRun = options.dryRun === true;
12
19
  const written = [];
13
20
 
14
- if (existsSync(configPath) && options.force !== true) {
21
+ if (!dryRun && existsSync(configPath) && options.force !== true) {
15
22
  throw new Error(`${path.relative(pluginRoot, configPath)} already exists; pass --force to overwrite it`);
16
23
  }
24
+ if (!dryRun && workflowPath && existsSync(workflowPath) && options.force !== true) {
25
+ throw new Error(`${path.relative(pluginRoot, workflowPath)} already exists; pass --force to overwrite it`);
26
+ }
27
+ const packageJsonPath = path.join(pluginRoot, "package.json");
28
+ const packageJson = options.scripts === true ? await readJsonIfExists(packageJsonPath) : null;
29
+ if (options.scripts === true) {
30
+ if (!packageJson) {
31
+ throw new Error("package.json is required to write plugin-inspector package scripts");
32
+ }
33
+ for (const name of Object.keys(defaultInitPackageScripts)) {
34
+ if (!dryRun && packageJson.scripts?.[name] && options.force !== true) {
35
+ throw new Error(`package.json scripts.${name} already exists; pass --force to overwrite it`);
36
+ }
37
+ }
38
+ }
17
39
 
18
40
  const config = await buildPluginInspectorConfig({ pluginRoot });
19
- await mkdir(path.dirname(configPath), { recursive: true });
20
- await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
41
+ if (!dryRun) {
42
+ await mkdir(path.dirname(configPath), { recursive: true });
43
+ await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
44
+ }
21
45
  written.push(configPath);
22
46
 
23
- if (options.ci === true) {
24
- const workflowPath = path.resolve(pluginRoot, options.workflowPath ?? defaultInitWorkflowPath);
25
- if (existsSync(workflowPath) && options.force !== true) {
26
- throw new Error(`${path.relative(pluginRoot, workflowPath)} already exists; pass --force to overwrite it`);
47
+ if (workflowPath) {
48
+ if (!dryRun) {
49
+ await mkdir(path.dirname(workflowPath), { recursive: true });
50
+ await writeFile(workflowPath, renderGithubActionsWorkflow({ packageManager }), "utf8");
27
51
  }
28
- await mkdir(path.dirname(workflowPath), { recursive: true });
29
- await writeFile(workflowPath, renderGithubActionsWorkflow({ packageManager: options.packageManager }), "utf8");
30
52
  written.push(workflowPath);
31
53
  }
32
54
 
33
- return { pluginRoot, configPath, written };
55
+ if (options.scripts === true) {
56
+ const existingScripts = packageJson.scripts ?? {};
57
+ packageJson.scripts = {
58
+ ...existingScripts,
59
+ ...defaultInitPackageScripts,
60
+ };
61
+ if (!dryRun) {
62
+ await writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, "utf8");
63
+ }
64
+ written.push(packageJsonPath);
65
+ }
66
+
67
+ return { pluginRoot, configPath, dryRun, packageManager, written };
34
68
  }
35
69
 
36
70
  export async function buildPluginInspectorConfig(options = {}) {
@@ -79,7 +113,7 @@ jobs:
79
113
  node-version: 24
80
114
  cache: ${setup.cache}
81
115
  ${setup.corepack ? " - run: corepack enable\n" : ""} - run: ${setup.install}
82
- - run: PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 ${setup.exec} @openclaw/plugin-inspector ci --no-openclaw --runtime --mock-sdk
116
+ - run: ${setup.exec} @openclaw/plugin-inspector ci --no-openclaw --runtime --mock-sdk --allow-execute
83
117
  - uses: actions/upload-artifact@v5
84
118
  if: always()
85
119
  with:
@@ -88,19 +122,62 @@ ${setup.corepack ? " - run: corepack enable\n" : ""} - run: ${setup.in
88
122
  `;
89
123
  }
90
124
 
125
+ export async function detectPackageManager(pluginRoot) {
126
+ const root = path.resolve(pluginRoot ?? process.cwd());
127
+ const packageJson = await readJsonIfExists(path.join(root, "package.json"));
128
+ const packageManager = packageJson?.packageManager;
129
+ if (typeof packageManager === "string") {
130
+ const [name] = packageManager.split("@");
131
+ if (["npm", "pnpm", "yarn", "bun"].includes(name)) {
132
+ return name;
133
+ }
134
+ }
135
+
136
+ if (existsSync(path.join(root, "pnpm-lock.yaml"))) {
137
+ return "pnpm";
138
+ }
139
+ if (existsSync(path.join(root, "yarn.lock"))) {
140
+ return "yarn";
141
+ }
142
+ if (existsSync(path.join(root, "bun.lockb")) || existsSync(path.join(root, "bun.lock"))) {
143
+ return "bun";
144
+ }
145
+ return "npm";
146
+ }
147
+
91
148
  function inferSourceRoot(packageJson) {
92
149
  const entrypoints = [
93
150
  packageJson?.openclaw?.entrypoint,
94
151
  ...(packageJson?.openclaw?.extensions ?? []),
95
152
  ...(packageJson?.openclaw?.runtimeExtensions ?? []),
153
+ ...entrypointStrings(packageJson?.exports?.["."]),
154
+ ...entrypointStrings(packageJson?.exports),
155
+ packageJson?.module,
156
+ packageJson?.main,
96
157
  ].filter((value) => typeof value === "string");
97
- const entrypoint = entrypoints[0] ?? packageJson?.exports?.["."] ?? packageJson?.main ?? "src/index.js";
98
- if (typeof entrypoint === "string" && entrypoint.startsWith("src/")) {
158
+ const entrypoint = entrypoints[0] ?? "src/index.js";
159
+ if (stripRelativePrefix(entrypoint).startsWith("src/")) {
99
160
  return "src";
100
161
  }
101
162
  return ".";
102
163
  }
103
164
 
165
+ function entrypointStrings(value) {
166
+ if (typeof value === "string") {
167
+ return [value];
168
+ }
169
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
170
+ return [];
171
+ }
172
+ return ["import", "default", "require", "node", "module"]
173
+ .map((key) => value[key])
174
+ .filter((item) => typeof item === "string");
175
+ }
176
+
177
+ function stripRelativePrefix(filePath) {
178
+ return filePath.replace(/^\.\//, "");
179
+ }
180
+
104
181
  async function readJsonIfExists(filePath) {
105
182
  if (!existsSync(filePath)) {
106
183
  return null;
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { mkdtemp, rm } from "node:fs/promises";
2
+ import { rmSync } from "node:fs";
3
+ import { mkdtemp } from "node:fs/promises";
3
4
  import { register } from "node:module";
4
5
  import os from "node:os";
5
6
  import path from "node:path";
@@ -26,13 +27,16 @@ async function run(options) {
26
27
  const pluginRoot = path.resolve(options.cwd ?? process.cwd(), options.pluginRoot ?? path.dirname(entrypoint));
27
28
  const workspace = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-mock-sdk-"));
28
29
 
29
- try {
30
- const { loaderPath } = await createMockSdkPackage(workspace, { pluginRoot });
31
- register(pathToFileURL(loaderPath));
32
- return await captureLinkedEntrypoint(entrypoint, options);
33
- } finally {
34
- await rm(workspace, { force: true, recursive: true });
35
- }
30
+ cleanupTempDirOnExit(workspace);
31
+ const { loaderPath } = await createMockSdkPackage(workspace, { pluginRoot });
32
+ register(pathToFileURL(loaderPath));
33
+ return await captureLinkedEntrypoint(entrypoint, options);
34
+ }
35
+
36
+ function cleanupTempDirOnExit(dir) {
37
+ process.once("exit", () => {
38
+ rmSync(dir, { force: true, recursive: true });
39
+ });
36
40
  }
37
41
 
38
42
  async function captureLinkedEntrypoint(entrypoint, options) {
package/src/report.js CHANGED
@@ -246,27 +246,79 @@ export async function writeReport(report, options = {}) {
246
246
  export async function writeCompatibilityReport(report, options = {}) {
247
247
  const outDir = path.resolve(options.cwd ?? process.cwd(), options.outDir ?? "reports");
248
248
  const basename = options.basename ?? "plugin-inspector-report";
249
- const jsonPath = path.join(outDir, `${basename}.json`);
250
- const markdownPath = path.join(outDir, `${basename}.md`);
251
- const issuesPath = path.join(outDir, options.issuesBasename ?? "plugin-inspector-issues.md");
249
+ const jsonPath = options.jsonPath ?? path.join(outDir, `${basename}.json`);
250
+ const markdownPath = options.markdownPath ?? path.join(outDir, `${basename}.md`);
251
+ const issuesPath = options.issuesPath ?? path.join(outDir, options.issuesBasename ?? "plugin-inspector-issues.md");
252
+ const markdownOptions = compatibilityRenderOptions(options, {
253
+ title: options.markdownTitle ?? options.title,
254
+ ...options.markdownOptions,
255
+ });
256
+ const issuesOptions = compatibilityRenderOptions(options, {
257
+ title: options.issuesTitle ?? options.title,
258
+ ...options.issuesOptions,
259
+ });
252
260
 
253
261
  return writeArtifacts(
254
262
  [
255
263
  { name: "jsonPath", path: jsonPath, json: report },
256
- { name: "markdownPath", path: markdownPath, markdown: renderCompatibilityMarkdownReport(report) },
257
- { name: "issuesPath", path: issuesPath, markdown: renderCompatibilityIssuesReport(report) },
264
+ { name: "markdownPath", path: markdownPath, markdown: renderCompatibilityMarkdownReport(report, markdownOptions) },
265
+ { name: "issuesPath", path: issuesPath, markdown: renderCompatibilityIssuesReport(report, issuesOptions) },
258
266
  ],
259
267
  { check: options.check },
260
268
  );
261
269
  }
262
270
 
263
- export function renderTextSummary(report) {
264
- return [
271
+ function compatibilityRenderOptions(options, overrides) {
272
+ const renderOptions = {
273
+ formatEvidence: options.formatEvidence,
274
+ severityLabels: options.severityLabels,
275
+ ...overrides,
276
+ };
277
+ return Object.fromEntries(Object.entries(renderOptions).filter(([, value]) => value !== undefined));
278
+ }
279
+
280
+ export function renderTextSummary(report, options = {}) {
281
+ const lines = [
265
282
  `Status: ${report.status.toUpperCase()}`,
266
283
  `Fixtures: ${report.summary.fixtureCount}`,
267
284
  `Breakages: ${report.summary.breakageCount}`,
285
+ ...(typeof report.summary.issueCount === "number" ? [`Issues: ${report.summary.issueCount}`] : []),
268
286
  `Logs: ${report.summary.logCount}`,
269
- ].join("\n");
287
+ ];
288
+ const artifacts = Object.entries(options.artifacts ?? {}).filter(([, filePath]) => Boolean(filePath));
289
+ if (artifacts.length > 0) {
290
+ lines.push("", "Reports:", ...artifacts.map(([name, filePath]) => `- ${artifactLabel(name)}: ${filePath}`));
291
+ }
292
+ const findings = topTextFindings(report, options.topFindings ?? 3);
293
+ if (findings.length > 0) {
294
+ lines.push("", "Top findings:", ...findings.map((finding) => `- ${finding}`));
295
+ }
296
+ return lines.join("\n");
297
+ }
298
+
299
+ function topTextFindings(report, limit) {
300
+ if (report.status === "pass" || limit <= 0) {
301
+ return [];
302
+ }
303
+ return [
304
+ ...(report.breakages ?? []).map((finding) => formatTextFinding(finding, "breakage")),
305
+ ...(report.issues ?? [])
306
+ .filter((issue) => issue.status === "blocking" || issue.severity === "P0" || issue.severity === "P1")
307
+ .map((issue) => formatTextFinding(issue, issue.severity ?? "issue")),
308
+ ...(report.warnings ?? []).map((finding) => formatTextFinding(finding, "warning")),
309
+ ].slice(0, limit);
310
+ }
311
+
312
+ function formatTextFinding(finding, fallbackLevel) {
313
+ const level = finding.level ?? finding.severity ?? fallbackLevel;
314
+ const code = finding.code ? ` ${finding.code}` : "";
315
+ const message = finding.message ?? finding.title ?? "see report";
316
+ const evidence = Array.isArray(finding.evidence) && finding.evidence.length > 0 ? ` (${finding.evidence[0]})` : "";
317
+ return `${String(level).toUpperCase()} ${finding.fixture ?? "unknown"}${code}: ${message}${evidence}`;
318
+ }
319
+
320
+ function artifactLabel(name) {
321
+ return String(name).replace(/Path$/u, "");
270
322
  }
271
323
 
272
324
  export function renderMarkdownReport(report) {