@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 ADDED
@@ -0,0 +1,21 @@
1
+ # Changelog
2
+
3
+ ## 0.1.1 - 2026-04-27
4
+
5
+ ### Changed
6
+
7
+ - Refresh npm package docs with the simplified CLI-first README and public API surface cleanup.
8
+
9
+ ## 0.1.0 - 2026-04-27
10
+
11
+ Initial public package release for `@openclaw/plugin-inspector`.
12
+
13
+ ### Added
14
+
15
+ - Plugin-root `plugin-inspector check` command with optional `plugin-inspector.config.json`.
16
+ - Static OpenClaw plugin compatibility reports, issue reports, and CI policy summaries.
17
+ - Crabpot-compatible fixture-set inspection and report assembly APIs.
18
+ - Target OpenClaw surface parsing for compat registry records, hook names, registrar names, SDK exports, and manifest type fields.
19
+ - Package metadata, manifest, SDK import, hook, registration, runtime-capture, cold-import, synthetic-probe, runtime-profile, ref-diff, and profile-diff report helpers.
20
+ - Optional `PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 plugin-inspector check --capture` runtime registration capture using a temporary mocked `openclaw/plugin-sdk`.
21
+ - 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,152 @@
1
- # @openclaw/plugin-inspector
1
+ <img src="docs/plugin-inspector-banner.jpg" alt="openclaw plugin inspector banner"/>
2
2
 
3
- Bootstrap placeholder. Use 0.1.0 or newer.
3
+ # 💊 OpenClaw Plugin Inspector
4
+
5
+ `plugin-inspector` is the reusable OpenClaw plugin compatibility inspector. It
6
+ wraps the static inspection, registration capture, and report model prototyped
7
+ in crabpot into an npm-publishable package.
8
+
9
+ ## Install
10
+
11
+ Install it as a dev dependency in a plugin repo:
12
+
13
+ ```bash
14
+ npm install --save-dev @openclaw/plugin-inspector
15
+ ```
16
+
17
+ Then run it from the plugin root:
18
+
19
+ ```bash
20
+ npx @openclaw/plugin-inspector check
21
+ ```
22
+
23
+ ## CLI
24
+
25
+ Run the default plugin-root check from a plugin package directory:
26
+
27
+ ```bash
28
+ plugin-inspector check
29
+ ```
30
+
31
+ That command reads the current directory as one plugin, inspects package
32
+ metadata, `openclaw.plugin.json`, source imports, `api.on(...)`,
33
+ `api.register*`, and writes:
34
+
35
+ - `reports/plugin-inspector-report.json`
36
+ - `reports/plugin-inspector-report.md`
37
+ - `reports/plugin-inspector-issues.md`
38
+
39
+ Use `--no-openclaw` when CI should not compare against a local OpenClaw
40
+ checkout:
41
+
42
+ ```bash
43
+ plugin-inspector check --no-openclaw
44
+ ```
45
+
46
+ Use a simple plugin-root config when you want stable fixture metadata or
47
+ expected seams:
48
+
49
+ ```json
50
+ {
51
+ "version": 1,
52
+ "plugin": {
53
+ "id": "weather",
54
+ "priority": "high",
55
+ "seams": ["dynamic-tool"],
56
+ "sourceRoot": "src",
57
+ "expect": {
58
+ "registrations": ["registerTool"]
59
+ }
60
+ },
61
+ "openclaw": {
62
+ "defaultCheckoutPath": "../openclaw"
63
+ }
64
+ }
65
+ ```
66
+
67
+ Then run:
68
+
69
+ ```bash
70
+ plugin-inspector check --config plugin-inspector.config.json
71
+ ```
72
+
73
+ Copy-ready examples live in `examples/plugin-inspector.config.json` and
74
+ `examples/github-actions-plugin-inspector.yml`.
75
+
76
+ Fixture-set configs are still supported for crabpot-style compatibility suites:
77
+
78
+ ```bash
79
+ plugin-inspector report --config crabpot.config.json --out reports
80
+ ```
81
+
82
+ Capture a plugin entrypoint in an explicitly isolated execution lane:
83
+
84
+ ```bash
85
+ PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 plugin-inspector capture ./dist/index.js --mock-sdk
86
+ ```
87
+
88
+ Run the optional runtime capture smoke during `check`:
89
+
90
+ ```bash
91
+ PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 plugin-inspector check --no-openclaw --capture
92
+ ```
93
+
94
+ Runtime capture creates a temporary mock `openclaw/plugin-sdk` package, imports
95
+ declared OpenClaw package entrypoints, calls their `register(api)` function with
96
+ the capture API, and writes:
97
+
98
+ - `reports/plugin-inspector-runtime-capture.json`
99
+ - `reports/plugin-inspector-runtime-capture.md`
100
+
101
+ ### CI
102
+
103
+ With a dev dependency:
104
+
105
+ ```json
106
+ {
107
+ "scripts": {
108
+ "plugin:check": "plugin-inspector check --no-openclaw",
109
+ "plugin:check:runtime": "PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 plugin-inspector check --no-openclaw --capture"
110
+ }
111
+ }
112
+ ```
113
+
114
+ GitHub Actions:
115
+
116
+ ```yaml
117
+ name: plugin-inspector
118
+
119
+ on:
120
+ pull_request:
121
+ push:
122
+ branches: [main]
123
+
124
+ jobs:
125
+ check:
126
+ runs-on: ubuntu-latest
127
+ steps:
128
+ - uses: actions/checkout@v5
129
+ - uses: actions/setup-node@v5
130
+ with:
131
+ node-version: 24
132
+ cache: npm
133
+ - run: npm ci
134
+ - run: npm run plugin:check
135
+ - run: npm run plugin:check:runtime
136
+ - uses: actions/upload-artifact@v5
137
+ if: always()
138
+ with:
139
+ name: plugin-inspector-reports
140
+ path: reports/plugin-inspector-*
141
+ ```
142
+
143
+ ## Scope
144
+
145
+ Default inspection is offline and credential-free. It reads manifests, package
146
+ metadata, and source files, then reports observed `api.on(...)`,
147
+ `api.register*`, `define*`, SDK imports, and manifest contracts.
148
+ OpenClaw target checkout parsing is limited to public compatibility registries,
149
+ SDK package exports, manifest types, hooks, and captured registrar metadata.
150
+
151
+ Cold import capture and synthetic contract probes are explicit opt-in modes.
152
+ 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,59 @@
1
1
  {
2
2
  "name": "@openclaw/plugin-inspector",
3
- "version": "0.0.0",
4
- "description": "OpenClaw plugin inspector bootstrap placeholder.",
5
- "license": "MIT"
3
+ "version": "0.1.1",
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
+ "./advanced": "./src/advanced.js",
26
+ "./capture-api": "./src/capture-api.js",
27
+ "./ci-policy": "./src/ci-policy.js",
28
+ "./cold-import-readiness": "./src/cold-import-readiness.js",
29
+ "./contract-capture": "./src/contract-capture.js",
30
+ "./contract-coverage": "./src/contract-coverage.js",
31
+ "./execution-results": "./src/execution-results.js",
32
+ "./import-loop-profile": "./src/import-loop-profile.js",
33
+ "./openclaw-target": "./src/openclaw-target.js",
34
+ "./platform-probes": "./src/platform-probes.js",
35
+ "./profile-diff": "./src/profile-diff.js",
36
+ "./ref-diff": "./src/ref-diff.js",
37
+ "./runtime-capture-report": "./src/runtime-capture-report.js",
38
+ "./runtime-profile": "./src/runtime-profile.js",
39
+ "./workspace-plan": "./src/workspace-plan.js"
40
+ },
41
+ "files": [
42
+ "src",
43
+ "examples",
44
+ "README.md",
45
+ "CHANGELOG.md",
46
+ "LICENSE"
47
+ ],
48
+ "scripts": {
49
+ "check": "npm test && npm pack --dry-run",
50
+ "release:local": "npm run check",
51
+ "test": "node --test test/*.test.js"
52
+ },
53
+ "keywords": [
54
+ "openclaw",
55
+ "plugin",
56
+ "compatibility",
57
+ "ci"
58
+ ]
6
59
  }
@@ -0,0 +1,186 @@
1
+ export {
2
+ escapeMarkdownTableCell,
3
+ renderArtifactContent,
4
+ renderMarkdownTable,
5
+ renderPaddedMarkdownTable,
6
+ writeArtifacts,
7
+ writeJsonMarkdownArtifacts,
8
+ } from "./artifacts.js";
9
+ export {
10
+ normalizeRepoPath,
11
+ posixJoin,
12
+ resolveFromRoot,
13
+ resolveRequiredFromRoot,
14
+ slugForArtifact,
15
+ toRepoPath,
16
+ } from "./path-utils.js";
17
+ export { readJsonFile, readOptionalJsonFile } from "./json-file.js";
18
+ export { assertRunCount, percentile } from "./stats.js";
19
+ export { createCaptureApi } from "./capture-api.js";
20
+ export {
21
+ buildCiPolicyReport,
22
+ defaultCiPolicyReportOptions,
23
+ renderCiPolicyMarkdown,
24
+ validateCiPolicy,
25
+ validateCiPolicyReport,
26
+ writeCiPolicyReport,
27
+ } from "./ci-policy.js";
28
+ export {
29
+ buildCiSummary,
30
+ defaultCiReportPaths,
31
+ deriveCiStatus,
32
+ readCiReports,
33
+ renderCiSummaryMarkdown,
34
+ writeCiSummary,
35
+ } from "./ci-summary.js";
36
+ export {
37
+ buildContractProbes,
38
+ contractProbeRules,
39
+ probePriority,
40
+ } from "./contract-probes.js";
41
+ export {
42
+ buildContractCapture,
43
+ defaultHookAssertions,
44
+ defaultHookContexts,
45
+ defaultHookEvents,
46
+ defaultRegistrationArguments,
47
+ defaultRegistrationAssertions,
48
+ renderContractCaptureMarkdown,
49
+ validateContractCapture,
50
+ writeContractCapture,
51
+ } from "./contract-capture.js";
52
+ export {
53
+ renderCompatibilityIssuesReport,
54
+ renderCompatibilityMarkdownReport,
55
+ } from "./compatibility-report.js";
56
+ export {
57
+ knownIssueClasses,
58
+ validateContractCoverage,
59
+ } from "./contract-coverage.js";
60
+ export {
61
+ buildColdImportReadiness,
62
+ renderColdImportReadinessMarkdown,
63
+ validateColdImportReadiness,
64
+ writeColdImportReadiness,
65
+ } from "./cold-import-readiness.js";
66
+ export {
67
+ buildIssues,
68
+ classifyIssueFinding,
69
+ deprecatedCompatRecords,
70
+ issueId,
71
+ issueMetadata,
72
+ issueMetadataByCode,
73
+ knownIssueCodes,
74
+ summarizeIssueClasses,
75
+ } from "./issues.js";
76
+ export {
77
+ buildExecutionResultsReport,
78
+ defaultExecutionResultsOptions,
79
+ renderExecutionResultsMarkdown,
80
+ writeExecutionResultsReport,
81
+ } from "./execution-results.js";
82
+ export {
83
+ buildCompatibilityFixtureReport,
84
+ classifyCompatibilityFixture,
85
+ classifyPackageContracts,
86
+ classifyTargetOpenClawCoverage,
87
+ readPackageSummaries,
88
+ readPluginManifests,
89
+ summarizePackage,
90
+ } from "./fixture-summary.js";
91
+ export {
92
+ buildImportLoopProfile,
93
+ defaultImportLoopProfileOptions,
94
+ renderImportLoopProfileMarkdown,
95
+ validateImportLoopProfile,
96
+ writeImportLoopProfile,
97
+ } from "./import-loop-profile.js";
98
+ export {
99
+ defaultOpenClawCheckoutPaths,
100
+ openClawTargetPathCandidates,
101
+ parseCompatRecordEntries,
102
+ parseExportedStringArray,
103
+ parsePluginSdkExports,
104
+ parseTypeFields,
105
+ readOpenClawTargetSurface,
106
+ } from "./openclaw-target.js";
107
+ export {
108
+ captureEntrypoint,
109
+ captureEntrypointWithMockSdk,
110
+ inspectCompatibilityFixtureSet,
111
+ inspectFixtureSet,
112
+ inspectPlugin,
113
+ inspectSourceText,
114
+ } from "./inspector.js";
115
+ export {
116
+ defaultPluginRootConfigFiles,
117
+ fixtureCheckoutPath,
118
+ fixtureSourceRoot,
119
+ loadInspectorConfig,
120
+ loadPluginRootConfig,
121
+ normalizeInspectorConfig,
122
+ normalizePluginRootConfig,
123
+ validateInspectorConfig,
124
+ } from "./config.js";
125
+ export {
126
+ buildPlatformProbes,
127
+ defaultPlatformTargets,
128
+ renderPlatformProbesMarkdown,
129
+ validatePlatformProbes,
130
+ writePlatformProbes,
131
+ } from "./platform-probes.js";
132
+ export {
133
+ buildProfileDiff,
134
+ defaultProfileDiffOptions,
135
+ renderProfileDiffMarkdown,
136
+ validateProfileDiff,
137
+ writeProfileDiff,
138
+ } from "./profile-diff.js";
139
+ export {
140
+ buildRefDiff,
141
+ defaultRefDiffDimensions,
142
+ defaultRefDiffOptions,
143
+ renderRefDiffMarkdown,
144
+ validateRefDiff,
145
+ writeRefDiff,
146
+ } from "./ref-diff.js";
147
+ export {
148
+ buildCompatibilityReport,
149
+ classifyCompatRecordCoverage,
150
+ renderMarkdownReport,
151
+ renderTextSummary,
152
+ writeCompatibilityReport,
153
+ writeReport,
154
+ } from "./report.js";
155
+ export {
156
+ buildRuntimeProfile,
157
+ defaultRuntimeProfileCommands,
158
+ defaultRuntimeProfileOptions,
159
+ renderRuntimeProfileMarkdown,
160
+ validateRuntimeProfile,
161
+ writeRuntimeProfile,
162
+ } from "./runtime-profile.js";
163
+ export {
164
+ buildRuntimeCaptureReport,
165
+ renderRuntimeCaptureMarkdown,
166
+ writeRuntimeCaptureReport,
167
+ } from "./runtime-capture-report.js";
168
+ export { createMockSdkPackage } from "./sdk-mock.js";
169
+ export {
170
+ buildSyntheticProbePlan,
171
+ defaultSyntheticHookContexts,
172
+ defaultSyntheticHookEvents,
173
+ defaultSyntheticRegistrationArguments,
174
+ renderSyntheticProbeMarkdown,
175
+ runCapturedSyntheticProbes,
176
+ syntheticRegistrationExecutionProfiles,
177
+ validateSyntheticProbePlan,
178
+ writeSyntheticProbePlan,
179
+ } from "./synthetic-probes.js";
180
+ export {
181
+ buildWorkspacePlan,
182
+ defaultWorkspacePlanOptions,
183
+ renderWorkspacePlanMarkdown,
184
+ validateWorkspacePlan,
185
+ writeWorkspacePlan,
186
+ } from "./workspace-plan.js";
package/src/api.js ADDED
@@ -0,0 +1,85 @@
1
+ import path from "node:path";
2
+ import { createCaptureApi } from "./capture-api.js";
3
+ import { loadInspectorConfig, loadPluginRootConfig } from "./config.js";
4
+ import { captureEntrypoint } from "./inspector.js";
5
+ import { renderTextSummary, writeCompatibilityReport } from "./report.js";
6
+ import { buildRuntimeCaptureReport, writeRuntimeCaptureReport } from "./runtime-capture-report.js";
7
+ import { inspectCompatibilityFixtureSet, inspectFixtureSet } from "./inspector.js";
8
+
9
+ export async function loadPluginConfig(options = {}) {
10
+ if (options.config) {
11
+ return options.config;
12
+ }
13
+ const cwd = options.pluginRoot ?? options.cwd;
14
+ if (options.configPath) {
15
+ return options.fixtureSet === true
16
+ ? loadInspectorConfig(options.configPath, { cwd })
17
+ : loadPluginRootConfig(options.configPath, { cwd });
18
+ }
19
+ return loadPluginRootConfig(null, { cwd });
20
+ }
21
+
22
+ export async function inspectPluginRoot(options = {}) {
23
+ const config = await loadPluginConfig(options);
24
+ return inspectCompatibilityFixtureSet(config, {
25
+ generatedAt: options.generatedAt,
26
+ openclawPath: options.openclawPath,
27
+ targetOpenClaw: options.targetOpenClaw,
28
+ });
29
+ }
30
+
31
+ export async function inspectFixtureSetConfig(options = {}) {
32
+ const config = options.config ?? (await loadInspectorConfig(options.configPath, { cwd: options.cwd }));
33
+ return inspectFixtureSet(config, { generatedAt: options.generatedAt });
34
+ }
35
+
36
+ export async function writePluginReports(report, options = {}) {
37
+ return writeCompatibilityReport(report, {
38
+ basename: options.basename,
39
+ check: options.check,
40
+ cwd: options.cwd ?? options.pluginRoot,
41
+ issuesBasename: options.issuesBasename,
42
+ outDir: options.outDir,
43
+ });
44
+ }
45
+
46
+ export async function runPluginCheck(options = {}) {
47
+ const outDir = options.outDir ?? "reports";
48
+ const report = await inspectPluginRoot(options);
49
+ const paths = await writePluginReports(report, { ...options, outDir });
50
+ const result = { report, paths };
51
+
52
+ if (options.capture === true) {
53
+ if (process.env.PLUGIN_INSPECTOR_EXECUTE_ISOLATED !== "1") {
54
+ throw new Error("runtime capture imports plugin code; rerun with PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 in an isolated workspace");
55
+ }
56
+ const config = await loadPluginConfig(options);
57
+ const runtimeCapture = await buildRuntimeCaptureReport({
58
+ mockSdk: options.mockSdk ?? true,
59
+ report,
60
+ rootDir: config.rootDir,
61
+ });
62
+ const outputRoot = options.cwd ?? options.pluginRoot ?? process.cwd();
63
+ const runtimeCapturePaths = await writeRuntimeCaptureReport(runtimeCapture, {
64
+ jsonPath: path.resolve(outputRoot, outDir, "plugin-inspector-runtime-capture.json"),
65
+ markdownPath: path.resolve(outputRoot, outDir, "plugin-inspector-runtime-capture.md"),
66
+ });
67
+ result.runtimeCapture = runtimeCapture;
68
+ result.runtimeCapturePaths = runtimeCapturePaths;
69
+ if (runtimeCapture.summary.failedCount > 0) {
70
+ throw new Error(`plugin-inspector runtime capture failed for ${runtimeCapture.summary.failedCount} entrypoints`);
71
+ }
72
+ }
73
+
74
+ if (options.failOnBreakages === true && report.status !== "pass") {
75
+ throw new Error(`plugin-inspector found ${report.summary.breakageCount} breakages`);
76
+ }
77
+
78
+ return result;
79
+ }
80
+
81
+ export async function capturePluginEntrypoint(entrypoint, options = {}) {
82
+ return captureEntrypoint(entrypoint, options);
83
+ }
84
+
85
+ export { createCaptureApi, renderTextSummary };
@@ -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
+ }