@openclaw/plugin-inspector 0.0.0 → 0.1.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.
- package/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +249 -2
- package/examples/github-actions-plugin-inspector.yml +24 -0
- package/examples/plugin-inspector.config.json +15 -0
- package/package.json +55 -3
- 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 +130 -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 +186 -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
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 - 2026-04-27
|
|
4
|
+
|
|
5
|
+
Initial public package release for `@openclaw/plugin-inspector`.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Plugin-root `plugin-inspector check` command with optional `plugin-inspector.config.json`.
|
|
10
|
+
- Static OpenClaw plugin compatibility reports, issue reports, and CI policy summaries.
|
|
11
|
+
- Crabpot-compatible fixture-set inspection and report assembly APIs.
|
|
12
|
+
- Target OpenClaw surface parsing for compat registry records, hook names, registrar names, SDK exports, and manifest type fields.
|
|
13
|
+
- Package metadata, manifest, SDK import, hook, registration, runtime-capture, cold-import, synthetic-probe, runtime-profile, ref-diff, and profile-diff report helpers.
|
|
14
|
+
- Optional `PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 plugin-inspector check --capture` runtime registration capture using a temporary mocked `openclaw/plugin-sdk`.
|
|
15
|
+
- Copy-ready config and GitHub Actions examples under `examples/`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 OpenClaw
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,250 @@
|
|
|
1
|
-
#
|
|
1
|
+
# plugin-inspector
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`plugin-inspector` is the reusable OpenClaw plugin compatibility inspector. It
|
|
4
|
+
wraps the static inspection, registration capture, and report model prototyped
|
|
5
|
+
in crabpot into an npm-publishable package.
|
|
6
|
+
|
|
7
|
+
No npm package has been published yet.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
During development, use a local checkout or packed tarball:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install --save-dev ../plugin-inspector
|
|
15
|
+
npx plugin-inspector check --no-openclaw
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
After the package is published, plugin repos should install it as a dev
|
|
19
|
+
dependency and run it from the plugin root:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install --save-dev @openclaw/plugin-inspector
|
|
23
|
+
npx @openclaw/plugin-inspector check
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## CLI
|
|
27
|
+
|
|
28
|
+
Run the default plugin-root check from a plugin package directory:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
plugin-inspector check
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
That command reads the current directory as one plugin, inspects package
|
|
35
|
+
metadata, `openclaw.plugin.json`, source imports, `api.on(...)`,
|
|
36
|
+
`api.register*`, and writes:
|
|
37
|
+
|
|
38
|
+
- `reports/plugin-inspector-report.json`
|
|
39
|
+
- `reports/plugin-inspector-report.md`
|
|
40
|
+
- `reports/plugin-inspector-issues.md`
|
|
41
|
+
|
|
42
|
+
Use `--no-openclaw` when CI should not compare against a local OpenClaw
|
|
43
|
+
checkout:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
plugin-inspector check --no-openclaw
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Use a simple plugin-root config when you want stable fixture metadata or
|
|
50
|
+
expected seams:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"version": 1,
|
|
55
|
+
"plugin": {
|
|
56
|
+
"id": "weather",
|
|
57
|
+
"priority": "high",
|
|
58
|
+
"seams": ["dynamic-tool"],
|
|
59
|
+
"sourceRoot": "src",
|
|
60
|
+
"expect": {
|
|
61
|
+
"registrations": ["registerTool"]
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"openclaw": {
|
|
65
|
+
"defaultCheckoutPath": "../openclaw"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Then run:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
plugin-inspector check --config plugin-inspector.config.json
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Copy-ready examples live in `examples/plugin-inspector.config.json` and
|
|
77
|
+
`examples/github-actions-plugin-inspector.yml`.
|
|
78
|
+
|
|
79
|
+
Fixture-set configs are still supported for crabpot-style compatibility suites:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
plugin-inspector report --config crabpot.config.json --out reports
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Capture a plugin entrypoint in an explicitly isolated execution lane:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 plugin-inspector capture ./dist/index.js --mock-sdk
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Run the optional runtime capture smoke during `check`:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 plugin-inspector check --no-openclaw --capture
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Runtime capture creates a temporary mock `openclaw/plugin-sdk` package, imports
|
|
98
|
+
declared OpenClaw package entrypoints, calls their `register(api)` function with
|
|
99
|
+
the capture API, and writes:
|
|
100
|
+
|
|
101
|
+
- `reports/plugin-inspector-runtime-capture.json`
|
|
102
|
+
- `reports/plugin-inspector-runtime-capture.md`
|
|
103
|
+
|
|
104
|
+
### CI
|
|
105
|
+
|
|
106
|
+
With a dev dependency:
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"scripts": {
|
|
111
|
+
"plugin:check": "plugin-inspector check --no-openclaw",
|
|
112
|
+
"plugin:check:runtime": "PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 plugin-inspector check --no-openclaw --capture"
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
GitHub Actions:
|
|
118
|
+
|
|
119
|
+
```yaml
|
|
120
|
+
name: plugin-inspector
|
|
121
|
+
|
|
122
|
+
on:
|
|
123
|
+
pull_request:
|
|
124
|
+
push:
|
|
125
|
+
branches: [main]
|
|
126
|
+
|
|
127
|
+
jobs:
|
|
128
|
+
check:
|
|
129
|
+
runs-on: ubuntu-latest
|
|
130
|
+
steps:
|
|
131
|
+
- uses: actions/checkout@v5
|
|
132
|
+
- uses: actions/setup-node@v5
|
|
133
|
+
with:
|
|
134
|
+
node-version: 24
|
|
135
|
+
cache: npm
|
|
136
|
+
- run: npm ci
|
|
137
|
+
- run: npm run plugin:check
|
|
138
|
+
- run: npm run plugin:check:runtime
|
|
139
|
+
- uses: actions/upload-artifact@v5
|
|
140
|
+
if: always()
|
|
141
|
+
with:
|
|
142
|
+
name: plugin-inspector-reports
|
|
143
|
+
path: reports/plugin-inspector-*
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## API
|
|
147
|
+
|
|
148
|
+
```js
|
|
149
|
+
import {
|
|
150
|
+
buildCiSummary,
|
|
151
|
+
buildCiPolicyReport,
|
|
152
|
+
buildColdImportReadiness,
|
|
153
|
+
buildContractCapture,
|
|
154
|
+
buildExecutionResultsReport,
|
|
155
|
+
buildImportLoopProfile,
|
|
156
|
+
buildPlatformProbes,
|
|
157
|
+
buildProfileDiff,
|
|
158
|
+
buildRefDiff,
|
|
159
|
+
buildRuntimeProfile,
|
|
160
|
+
buildRuntimeCaptureReport,
|
|
161
|
+
buildWorkspacePlan,
|
|
162
|
+
createCaptureApi,
|
|
163
|
+
inspectFixtureSet,
|
|
164
|
+
loadInspectorConfig,
|
|
165
|
+
readOpenClawTargetSurface,
|
|
166
|
+
renderCiPolicyMarkdown,
|
|
167
|
+
renderColdImportReadinessMarkdown,
|
|
168
|
+
renderContractCaptureMarkdown,
|
|
169
|
+
renderExecutionResultsMarkdown,
|
|
170
|
+
renderImportLoopProfileMarkdown,
|
|
171
|
+
renderPlatformProbesMarkdown,
|
|
172
|
+
renderProfileDiffMarkdown,
|
|
173
|
+
renderRefDiffMarkdown,
|
|
174
|
+
renderRuntimeProfileMarkdown,
|
|
175
|
+
renderRuntimeCaptureMarkdown,
|
|
176
|
+
renderWorkspacePlanMarkdown,
|
|
177
|
+
renderMarkdownReport,
|
|
178
|
+
validateCiPolicyReport,
|
|
179
|
+
validateContractCoverage,
|
|
180
|
+
writeCiSummary,
|
|
181
|
+
writeCiPolicyReport,
|
|
182
|
+
writeColdImportReadiness,
|
|
183
|
+
writeContractCapture,
|
|
184
|
+
writeExecutionResultsReport,
|
|
185
|
+
writeImportLoopProfile,
|
|
186
|
+
writePlatformProbes,
|
|
187
|
+
writeProfileDiff,
|
|
188
|
+
writeRefDiff,
|
|
189
|
+
writeRuntimeProfile,
|
|
190
|
+
writeRuntimeCaptureReport,
|
|
191
|
+
writeWorkspacePlan,
|
|
192
|
+
writeReport,
|
|
193
|
+
} from "@openclaw/plugin-inspector";
|
|
194
|
+
|
|
195
|
+
const config = await loadInspectorConfig("crabpot.config.json");
|
|
196
|
+
const report = await inspectFixtureSet(config);
|
|
197
|
+
await writeReport(report, { outDir: "reports" });
|
|
198
|
+
|
|
199
|
+
const summary = await buildCiSummary({ reportsDir: "reports" });
|
|
200
|
+
await writeCiSummary(summary);
|
|
201
|
+
|
|
202
|
+
const policyReport = buildCiPolicyReport({ policy, compatibilityReport: report });
|
|
203
|
+
await writeCiPolicyReport(policyReport);
|
|
204
|
+
|
|
205
|
+
const capture = buildContractCapture({ report });
|
|
206
|
+
await writeContractCapture(capture);
|
|
207
|
+
const coverageErrors = validateContractCoverage(report);
|
|
208
|
+
|
|
209
|
+
const readiness = buildColdImportReadiness({ report });
|
|
210
|
+
await writeColdImportReadiness(readiness);
|
|
211
|
+
|
|
212
|
+
const target = await readOpenClawTargetSurface({ manifest: config });
|
|
213
|
+
|
|
214
|
+
const workspacePlan = await buildWorkspacePlan({ report, readiness });
|
|
215
|
+
await writeWorkspacePlan(workspacePlan);
|
|
216
|
+
|
|
217
|
+
const platformProbes = buildPlatformProbes({ plan: workspacePlan });
|
|
218
|
+
await writePlatformProbes(platformProbes);
|
|
219
|
+
|
|
220
|
+
const executionResults = await buildExecutionResultsReport({ resultsDir: ".plugin-inspector/results" });
|
|
221
|
+
await writeExecutionResultsReport(executionResults);
|
|
222
|
+
|
|
223
|
+
const importLoop = await buildImportLoopProfile({ entrypoint: "dist/index.js", runs: 3 });
|
|
224
|
+
await writeImportLoopProfile(importLoop);
|
|
225
|
+
|
|
226
|
+
const runtimeProfile = await buildRuntimeProfile({
|
|
227
|
+
commands: [{ id: "node-boot", label: "Node boot", category: "baseline", args: ["-e", "0"] }],
|
|
228
|
+
});
|
|
229
|
+
await writeRuntimeProfile(runtimeProfile);
|
|
230
|
+
|
|
231
|
+
const runtimeCapture = await buildRuntimeCaptureReport({ report, rootDir: process.cwd() });
|
|
232
|
+
await writeRuntimeCaptureReport(runtimeCapture);
|
|
233
|
+
|
|
234
|
+
const refDiff = await buildRefDiff({ baseReport, headReport });
|
|
235
|
+
await writeRefDiff(refDiff);
|
|
236
|
+
|
|
237
|
+
const profileDiff = await buildProfileDiff({ current, baseline, policy });
|
|
238
|
+
await writeProfileDiff(profileDiff);
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Scope
|
|
242
|
+
|
|
243
|
+
Default inspection is offline and credential-free. It reads manifests, package
|
|
244
|
+
metadata, and source files, then reports observed `api.on(...)`,
|
|
245
|
+
`api.register*`, `define*`, SDK imports, and manifest contracts.
|
|
246
|
+
OpenClaw target checkout parsing is limited to public compatibility registries,
|
|
247
|
+
SDK package exports, manifest types, hooks, and captured registrar metadata.
|
|
248
|
+
|
|
249
|
+
Cold import capture and synthetic contract probes are explicit opt-in modes.
|
|
250
|
+
Live lanes will stay credential-gated and must never run in default CI.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: plugin-inspector
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
push:
|
|
6
|
+
branches: [main]
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
check:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v5
|
|
13
|
+
- uses: actions/setup-node@v5
|
|
14
|
+
with:
|
|
15
|
+
node-version: 24
|
|
16
|
+
cache: npm
|
|
17
|
+
- run: npm ci
|
|
18
|
+
- run: npm run plugin:check
|
|
19
|
+
- run: npm run plugin:check:runtime
|
|
20
|
+
- uses: actions/upload-artifact@v5
|
|
21
|
+
if: always()
|
|
22
|
+
with:
|
|
23
|
+
name: plugin-inspector-reports
|
|
24
|
+
path: reports/plugin-inspector-*
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"plugin": {
|
|
4
|
+
"id": "weather",
|
|
5
|
+
"priority": "high",
|
|
6
|
+
"seams": ["dynamic-tool"],
|
|
7
|
+
"sourceRoot": "src",
|
|
8
|
+
"expect": {
|
|
9
|
+
"registrations": ["registerTool"]
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"openclaw": {
|
|
13
|
+
"defaultCheckoutPath": "../openclaw"
|
|
14
|
+
}
|
|
15
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,58 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/plugin-inspector",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Offline compatibility inspector for OpenClaw plugins.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"author": "OpenClaw",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/openclaw/plugin-inspector.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/openclaw/plugin-inspector/issues"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://github.com/openclaw/plugin-inspector#readme",
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=22"
|
|
19
|
+
},
|
|
20
|
+
"bin": {
|
|
21
|
+
"plugin-inspector": "src/cli.js"
|
|
22
|
+
},
|
|
23
|
+
"exports": {
|
|
24
|
+
".": "./src/index.js",
|
|
25
|
+
"./capture-api": "./src/capture-api.js",
|
|
26
|
+
"./ci-policy": "./src/ci-policy.js",
|
|
27
|
+
"./cold-import-readiness": "./src/cold-import-readiness.js",
|
|
28
|
+
"./contract-capture": "./src/contract-capture.js",
|
|
29
|
+
"./contract-coverage": "./src/contract-coverage.js",
|
|
30
|
+
"./execution-results": "./src/execution-results.js",
|
|
31
|
+
"./import-loop-profile": "./src/import-loop-profile.js",
|
|
32
|
+
"./openclaw-target": "./src/openclaw-target.js",
|
|
33
|
+
"./platform-probes": "./src/platform-probes.js",
|
|
34
|
+
"./profile-diff": "./src/profile-diff.js",
|
|
35
|
+
"./ref-diff": "./src/ref-diff.js",
|
|
36
|
+
"./runtime-capture-report": "./src/runtime-capture-report.js",
|
|
37
|
+
"./runtime-profile": "./src/runtime-profile.js",
|
|
38
|
+
"./workspace-plan": "./src/workspace-plan.js"
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"src",
|
|
42
|
+
"examples",
|
|
43
|
+
"README.md",
|
|
44
|
+
"CHANGELOG.md",
|
|
45
|
+
"LICENSE"
|
|
46
|
+
],
|
|
47
|
+
"scripts": {
|
|
48
|
+
"check": "npm test && npm pack --dry-run && npm publish --dry-run --access public",
|
|
49
|
+
"release:local": "npm run check",
|
|
50
|
+
"test": "node --test test/*.test.js"
|
|
51
|
+
},
|
|
52
|
+
"keywords": [
|
|
53
|
+
"openclaw",
|
|
54
|
+
"plugin",
|
|
55
|
+
"compatibility",
|
|
56
|
+
"ci"
|
|
57
|
+
]
|
|
6
58
|
}
|
package/src/artifacts.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export async function writeArtifacts(artifacts, options = {}) {
|
|
5
|
+
if (!Array.isArray(artifacts) || artifacts.length === 0) {
|
|
6
|
+
throw new TypeError("writeArtifacts requires at least one artifact");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const written = {};
|
|
10
|
+
for (const artifact of artifacts) {
|
|
11
|
+
const artifactPath = artifact.path;
|
|
12
|
+
if (!artifactPath) {
|
|
13
|
+
throw new TypeError("artifact.path is required");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const content = renderArtifactContent(artifact);
|
|
17
|
+
await mkdir(path.dirname(artifactPath), { recursive: true });
|
|
18
|
+
await writeFile(artifactPath, content, "utf8");
|
|
19
|
+
|
|
20
|
+
if (options.check || artifact.check) {
|
|
21
|
+
await assertFileMatches(artifactPath, content);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (artifact.name) {
|
|
25
|
+
written[artifact.name] = artifactPath;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return written;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function writeJsonMarkdownArtifacts({ jsonPath, markdownPath, json, markdown, check = false }) {
|
|
33
|
+
await writeArtifacts(
|
|
34
|
+
[
|
|
35
|
+
{ name: "jsonPath", path: jsonPath, json },
|
|
36
|
+
{ name: "markdownPath", path: markdownPath, markdown },
|
|
37
|
+
],
|
|
38
|
+
{ check },
|
|
39
|
+
);
|
|
40
|
+
return { jsonPath, markdownPath };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function renderArtifactContent(artifact) {
|
|
44
|
+
if ("content" in artifact) {
|
|
45
|
+
return String(artifact.content);
|
|
46
|
+
}
|
|
47
|
+
if ("json" in artifact) {
|
|
48
|
+
return `${JSON.stringify(artifact.json, null, 2)}\n`;
|
|
49
|
+
}
|
|
50
|
+
if ("markdown" in artifact) {
|
|
51
|
+
return `${artifact.markdown}\n`;
|
|
52
|
+
}
|
|
53
|
+
throw new TypeError("artifact must provide content, json, or markdown");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function renderMarkdownTable(rows, headers, options = {}) {
|
|
57
|
+
if (rows.length === 0 && options.empty != null) {
|
|
58
|
+
return options.empty;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const nullValue = options.nullValue ?? "";
|
|
62
|
+
const escape = options.escape !== false;
|
|
63
|
+
const normalizedRows = [headers, ...rows].map((row) =>
|
|
64
|
+
row.map((cell) => {
|
|
65
|
+
const value = String(cell ?? nullValue);
|
|
66
|
+
return escape ? escapeMarkdownTableCell(value) : value;
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
if (options.padding) {
|
|
71
|
+
const widths = headers.map((_, columnIndex) =>
|
|
72
|
+
Math.max(...normalizedRows.map((row) => row[columnIndex].length)),
|
|
73
|
+
);
|
|
74
|
+
const renderRow = (row) => `| ${row.map((cell, index) => cell.padEnd(widths[index])).join(" | ")} |`;
|
|
75
|
+
return [
|
|
76
|
+
renderRow(normalizedRows[0]),
|
|
77
|
+
renderRow(widths.map((width) => "-".repeat(width))),
|
|
78
|
+
...normalizedRows.slice(1).map(renderRow),
|
|
79
|
+
].join("\n");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const separator = headers.map(() => options.separator ?? "---");
|
|
83
|
+
return [normalizedRows[0], separator, ...normalizedRows.slice(1)]
|
|
84
|
+
.map((row) => `| ${row.join(" | ")} |`)
|
|
85
|
+
.join("\n");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function renderPaddedMarkdownTable(rows, headers, options = {}) {
|
|
89
|
+
return renderMarkdownTable(rows, headers, {
|
|
90
|
+
empty: "_none_",
|
|
91
|
+
escape: false,
|
|
92
|
+
padding: true,
|
|
93
|
+
...options,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function escapeMarkdownTableCell(value) {
|
|
98
|
+
return value.replace(/\|/g, "\\|").replace(/\n/g, "<br>");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function assertFileMatches(filePath, expected) {
|
|
102
|
+
try {
|
|
103
|
+
const actual = await readFile(filePath, "utf8");
|
|
104
|
+
if (actual !== expected) {
|
|
105
|
+
throw new Error(`${filePath} is not up to date`);
|
|
106
|
+
}
|
|
107
|
+
} catch (error) {
|
|
108
|
+
if (error?.code === "ENOENT") {
|
|
109
|
+
throw new Error(`${filePath} is missing`);
|
|
110
|
+
}
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
export function createCaptureApi(options = {}) {
|
|
2
|
+
const captured = [];
|
|
3
|
+
const retained = [];
|
|
4
|
+
const knownRegistrars = new Set(options.knownRegistrars ?? []);
|
|
5
|
+
const retainHandlers = options.retainHandlers === true;
|
|
6
|
+
|
|
7
|
+
const api = new Proxy(
|
|
8
|
+
{
|
|
9
|
+
config: options.config ?? {},
|
|
10
|
+
logger: options.logger ?? console,
|
|
11
|
+
pluginConfig: options.pluginConfig ?? {},
|
|
12
|
+
runtime: options.runtime ?? {},
|
|
13
|
+
on(name, handler) {
|
|
14
|
+
const captureIndex =
|
|
15
|
+
captured.push({
|
|
16
|
+
kind: "hook",
|
|
17
|
+
name,
|
|
18
|
+
handlerType: typeof handler,
|
|
19
|
+
arguments: summarizeArguments([name, handler]),
|
|
20
|
+
}) - 1;
|
|
21
|
+
if (retainHandlers) {
|
|
22
|
+
retained.push({
|
|
23
|
+
kind: "hook",
|
|
24
|
+
name,
|
|
25
|
+
handler,
|
|
26
|
+
captureIndex,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return api;
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
get(target, property) {
|
|
34
|
+
if (property === "getCapturedContracts") {
|
|
35
|
+
return () => captured.map((entry) => ({ ...entry }));
|
|
36
|
+
}
|
|
37
|
+
if (property === "getRetainedContracts") {
|
|
38
|
+
return () => retained.map((entry) => ({ ...entry }));
|
|
39
|
+
}
|
|
40
|
+
if (property in target) {
|
|
41
|
+
return target[property];
|
|
42
|
+
}
|
|
43
|
+
if (typeof property === "string" && isRegistrarProperty(property)) {
|
|
44
|
+
return (...args) => {
|
|
45
|
+
const captureIndex =
|
|
46
|
+
captured.push({
|
|
47
|
+
kind: "registration",
|
|
48
|
+
name: property,
|
|
49
|
+
known: knownRegistrars.size === 0 ? null : knownRegistrars.has(property),
|
|
50
|
+
arguments: summarizeArguments(args),
|
|
51
|
+
}) - 1;
|
|
52
|
+
if (retainHandlers) {
|
|
53
|
+
retained.push({
|
|
54
|
+
kind: "registration",
|
|
55
|
+
name: property,
|
|
56
|
+
arguments: args,
|
|
57
|
+
captureIndex,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return registrationReturnValue(property, args);
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
return api;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isRegistrarProperty(property) {
|
|
72
|
+
return property.startsWith("register") || property.startsWith("define");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function registrationReturnValue(name, args) {
|
|
76
|
+
if (name === "registerService") {
|
|
77
|
+
return {
|
|
78
|
+
name: objectName(args[0]),
|
|
79
|
+
start: async () => undefined,
|
|
80
|
+
stop: async () => undefined,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return objectName(args[0]) ?? undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function summarizeArguments(args) {
|
|
87
|
+
return args.map((arg) => summarizeValue(arg));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function summarizeValue(value) {
|
|
91
|
+
if (typeof value === "function") {
|
|
92
|
+
return { type: "function" };
|
|
93
|
+
}
|
|
94
|
+
if (Array.isArray(value)) {
|
|
95
|
+
return { type: "array", length: value.length };
|
|
96
|
+
}
|
|
97
|
+
if (value && typeof value === "object") {
|
|
98
|
+
return {
|
|
99
|
+
type: "object",
|
|
100
|
+
keys: Object.keys(value).sort(),
|
|
101
|
+
name: objectName(value),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
return { type: typeof value, value };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function objectName(value) {
|
|
108
|
+
if (!value || typeof value !== "object") {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
if (typeof value.name === "string") {
|
|
112
|
+
return value.name;
|
|
113
|
+
}
|
|
114
|
+
return typeof value.id === "string" ? value.id : null;
|
|
115
|
+
}
|