@openclaw/plugin-inspector 0.3.15 → 0.3.17
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 +13 -0
- package/package.json +1 -1
- package/src/capture-api.js +1 -1
- package/src/cli.js +4 -4
- package/src/fixture-summary.js +28 -3
- package/src/init.js +2 -1
- package/src/inspector.js +68 -13
- package/src/issues.js +70 -9
- package/src/json-file.js +98 -1
- package/src/profile-diff.js +7 -3
- package/src/prune-workspace-dev-deps-cli.js +3 -2
- package/src/report.js +1 -0
- package/src/sdk-deprecation-rules.js +99 -36
- package/src/sdk-mock.js +75 -2
- package/src/workspace-plan.js +23 -14
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.3.17 - 2026-06-29
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Detect deprecated session SDK read/write, file-path, and transcript helpers across source files, packaged `dist`/`build` artifacts, runtime session APIs, and dynamic SDK imports.
|
|
10
|
+
|
|
11
|
+
## 0.3.16 - 2026-06-23
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Write `package.json` updates atomically for `init --scripts` and `prune-workspace-dev-deps`, including symlink-preserving target rewrites and temp-file cleanup on staging failures. Thanks @KrasimirKralev.
|
|
16
|
+
- Mock the Lark SDK HTTP interceptor surface during runtime capture so Feishu plugin bundles load successfully.
|
|
17
|
+
|
|
5
18
|
## 0.3.15 - 2026-06-12
|
|
6
19
|
|
|
7
20
|
### Fixed
|
package/package.json
CHANGED
package/src/capture-api.js
CHANGED
|
@@ -151,7 +151,7 @@ export function createCaptureContext(options = {}) {
|
|
|
151
151
|
resolvePath: options.resolvePath ?? ((value) => value),
|
|
152
152
|
runtime: options.runtime ?? createRuntimeContext(options),
|
|
153
153
|
secrets: options.secrets ?? createSecretContext(options),
|
|
154
|
-
store: options.store ?? createStoreContext(
|
|
154
|
+
store: options.store ?? createStoreContext(),
|
|
155
155
|
paths: options.paths ?? {
|
|
156
156
|
cacheDir: ".plugin-inspector/cache",
|
|
157
157
|
configDir: ".plugin-inspector/config",
|
package/src/cli.js
CHANGED
|
@@ -38,7 +38,7 @@ try {
|
|
|
38
38
|
await runConfig(commandArgs);
|
|
39
39
|
} else if (command === "inspect" || command === "report") {
|
|
40
40
|
if (command === "inspect" && !commandArgs.includes("--config")) {
|
|
41
|
-
await runCheck(commandArgs);
|
|
41
|
+
await runCheck(commandArgs, { check: commandArgs.includes("--check") });
|
|
42
42
|
} else {
|
|
43
43
|
await runReport(command, commandArgs);
|
|
44
44
|
}
|
|
@@ -100,7 +100,7 @@ async function runConfig(commandArgs) {
|
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
async function runCheck(commandArgs) {
|
|
103
|
+
async function runCheck(commandArgs, options = {}) {
|
|
104
104
|
const configPath = readFlag(commandArgs, "--config");
|
|
105
105
|
const pluginRoot = readFlag(commandArgs, "--plugin-root") ?? readFlag(commandArgs, "--root");
|
|
106
106
|
const outDir = readFlag(commandArgs, "--out") ?? "reports";
|
|
@@ -133,7 +133,7 @@ async function runCheck(commandArgs) {
|
|
|
133
133
|
console.log(renderTextSummary(report, { artifacts: paths }));
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
if (report.status !== "pass") {
|
|
136
|
+
if ((options.check ?? true) && report.status !== "pass") {
|
|
137
137
|
throw new Error(`plugin-inspector found ${report.summary.breakageCount} breakages`);
|
|
138
138
|
}
|
|
139
139
|
}
|
|
@@ -288,7 +288,7 @@ async function runCapture(commandArgs) {
|
|
|
288
288
|
const entrypoint = findCaptureEntrypoint(commandArgs);
|
|
289
289
|
const outputPath = readFlag(commandArgs, "--output");
|
|
290
290
|
const pluginRoot = readFlag(commandArgs, "--plugin-root");
|
|
291
|
-
const mockSdk = readMockSdkFlag(commandArgs) ??
|
|
291
|
+
const mockSdk = readMockSdkFlag(commandArgs) ?? true;
|
|
292
292
|
const allowExecution = readAllowExecutionFlag(commandArgs);
|
|
293
293
|
if (!entrypoint) {
|
|
294
294
|
throw new Error("capture requires an entrypoint path");
|
package/src/fixture-summary.js
CHANGED
|
@@ -724,14 +724,39 @@ function classifySdkDeprecations({ fixture, inspection, fixtureReport, warnings,
|
|
|
724
724
|
decisions.push({
|
|
725
725
|
fixture: fixture.id,
|
|
726
726
|
decision: "core-compat-adapter",
|
|
727
|
-
seam:
|
|
728
|
-
action:
|
|
729
|
-
"Keep loadSessionStore compatibility active while plugin authors migrate to row-scoped session helpers.",
|
|
727
|
+
seam: sdkDeprecationSeamForCode(code),
|
|
728
|
+
action: sdkDeprecationActionForCode(code),
|
|
730
729
|
evidence: findings.map((finding) => finding.ref).join(", "),
|
|
731
730
|
});
|
|
732
731
|
}
|
|
733
732
|
}
|
|
734
733
|
|
|
734
|
+
function sdkDeprecationSeamForCode(code) {
|
|
735
|
+
if (code === "sdk-session-file-helper") {
|
|
736
|
+
return "session-file";
|
|
737
|
+
}
|
|
738
|
+
if (code === "sdk-session-transcript-file-target" || code === "sdk-session-transcript-low-level") {
|
|
739
|
+
return "session-transcript";
|
|
740
|
+
}
|
|
741
|
+
return "session-store";
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function sdkDeprecationActionForCode(code) {
|
|
745
|
+
if (code === "sdk-session-store-write") {
|
|
746
|
+
return "Keep whole-store session write compatibility active while plugin authors migrate to row-scoped session write helpers.";
|
|
747
|
+
}
|
|
748
|
+
if (code === "sdk-session-file-helper") {
|
|
749
|
+
return "Keep session file-path compatibility active while plugin authors migrate to session entry and transcript identity helpers.";
|
|
750
|
+
}
|
|
751
|
+
if (code === "sdk-session-transcript-file-target") {
|
|
752
|
+
return "Keep legacy transcript file target compatibility active while plugin authors migrate to structured transcript targets.";
|
|
753
|
+
}
|
|
754
|
+
if (code === "sdk-session-transcript-low-level") {
|
|
755
|
+
return "Keep low-level transcript write compatibility active while plugin authors migrate to structured transcript runtime helpers.";
|
|
756
|
+
}
|
|
757
|
+
return "Keep loadSessionStore compatibility active while plugin authors migrate to row-scoped session helpers.";
|
|
758
|
+
}
|
|
759
|
+
|
|
735
760
|
function classifySecurityManifestCoverage({ fixture, fixtureReport, warnings, decisions }) {
|
|
736
761
|
for (const securityManifest of fixtureReport.securityManifests ?? []) {
|
|
737
762
|
warnings.push({
|
package/src/init.js
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { inferPluginSeams, packageId } from "./config.js";
|
|
5
|
+
import { writeJsonFileAtomic } from "./json-file.js";
|
|
5
6
|
|
|
6
7
|
export const defaultInitConfigPath = "plugin-inspector.config.json";
|
|
7
8
|
export const defaultInitWorkflowPath = ".github/workflows/plugin-inspector.yml";
|
|
@@ -59,7 +60,7 @@ export async function writePluginInspectorInit(options = {}) {
|
|
|
59
60
|
...defaultInitPackageScripts,
|
|
60
61
|
};
|
|
61
62
|
if (!dryRun) {
|
|
62
|
-
await
|
|
63
|
+
await writeJsonFileAtomic(packageJsonPath, packageJson);
|
|
63
64
|
}
|
|
64
65
|
written.push(packageJsonPath);
|
|
65
66
|
}
|
package/src/inspector.js
CHANGED
|
@@ -95,10 +95,17 @@ export async function inspectPlugin(fixture, options = {}) {
|
|
|
95
95
|
return emptyInspection(fixture, "missing");
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
const
|
|
98
|
+
const packageInspection = await readPackageMetadata(config, checkoutPath, sourceRoot);
|
|
99
|
+
const includeBuildArtifacts = shouldScanBuildArtifacts(fixture, packageInspection);
|
|
100
|
+
const files = await listSourceFiles(sourceRoot, {
|
|
101
|
+
includeBuild: includeBuildArtifacts,
|
|
102
|
+
includeDist: includeBuildArtifacts,
|
|
103
|
+
});
|
|
99
104
|
if (sourceRoot !== checkoutPath) {
|
|
100
105
|
files.push(...(await listSourceFiles(checkoutPath, { shallowRootOnly: true })));
|
|
101
106
|
}
|
|
107
|
+
files.push(...packageInspection.entrypointFiles);
|
|
108
|
+
const sourceFiles = uniquePaths(files);
|
|
102
109
|
|
|
103
110
|
const hooks = new Set();
|
|
104
111
|
const registrations = new Set();
|
|
@@ -107,7 +114,7 @@ export async function inspectPlugin(fixture, options = {}) {
|
|
|
107
114
|
const sdkImportDetails = [];
|
|
108
115
|
const sdkDeprecationDetails = [];
|
|
109
116
|
|
|
110
|
-
for (const filePath of
|
|
117
|
+
for (const filePath of sourceFiles) {
|
|
111
118
|
const text = await readFile(filePath, "utf8");
|
|
112
119
|
const relativePath = path.relative(config.rootDir ?? process.cwd(), filePath);
|
|
113
120
|
const sourceInspection = inspectSourceText(text, relativePath);
|
|
@@ -129,8 +136,6 @@ export async function inspectPlugin(fixture, options = {}) {
|
|
|
129
136
|
}
|
|
130
137
|
|
|
131
138
|
const manifestInspection = await readManifestContracts(config, checkoutPath, sourceRoot);
|
|
132
|
-
const packageInspection = await readPackageMetadata(config, checkoutPath, sourceRoot);
|
|
133
|
-
|
|
134
139
|
return {
|
|
135
140
|
id: fixture.id,
|
|
136
141
|
status: "ok",
|
|
@@ -146,7 +151,7 @@ export async function inspectPlugin(fixture, options = {}) {
|
|
|
146
151
|
packageEntrypoints: packageInspection.entrypoints,
|
|
147
152
|
sdkImports: uniqueDetails(sdkImportDetails),
|
|
148
153
|
sdkDeprecations: uniqueSdkDeprecations(sdkDeprecationDetails),
|
|
149
|
-
sourceFiles:
|
|
154
|
+
sourceFiles: sourceFiles.map((filePath) => path.relative(config.rootDir ?? process.cwd(), filePath)).sort(),
|
|
150
155
|
};
|
|
151
156
|
}
|
|
152
157
|
|
|
@@ -418,18 +423,23 @@ async function readPackageMetadata(config, checkoutPath, sourceRoot) {
|
|
|
418
423
|
const files = [];
|
|
419
424
|
const errors = [];
|
|
420
425
|
const entrypoints = new Set();
|
|
426
|
+
const entrypointFiles = new Set();
|
|
421
427
|
|
|
422
428
|
for (const packageFile of packageFiles) {
|
|
423
429
|
const relativePath = path.relative(config.rootDir ?? process.cwd(), packageFile);
|
|
424
430
|
files.push(relativePath);
|
|
425
431
|
try {
|
|
426
432
|
const packageJson = JSON.parse(await readFile(packageFile, "utf8"));
|
|
427
|
-
|
|
428
|
-
collectEntrypoint(entrypoints, packageJson.
|
|
429
|
-
collectEntrypoint(entrypoints, packageJson.
|
|
430
|
-
collectEntrypoint(entrypoints, packageJson.openclaw?.
|
|
431
|
-
collectEntrypoint(entrypoints, packageJson.
|
|
432
|
-
collectEntrypoint(entrypoints, packageJson.
|
|
433
|
+
const packageDir = path.dirname(packageFile);
|
|
434
|
+
collectEntrypoint(entrypoints, entrypointFiles, packageDir, packageJson.main);
|
|
435
|
+
collectEntrypoint(entrypoints, entrypointFiles, packageDir, packageJson.module);
|
|
436
|
+
collectEntrypoint(entrypoints, entrypointFiles, packageDir, packageJson.openclaw?.entry);
|
|
437
|
+
collectEntrypoint(entrypoints, entrypointFiles, packageDir, packageJson.openclaw?.entrypoint);
|
|
438
|
+
collectEntrypoint(entrypoints, entrypointFiles, packageDir, packageJson.openclaw?.setupEntry);
|
|
439
|
+
collectEntrypoint(entrypoints, entrypointFiles, packageDir, packageJson.exports?.["."]?.import);
|
|
440
|
+
collectEntrypoint(entrypoints, entrypointFiles, packageDir, packageJson.exports?.["."]?.default);
|
|
441
|
+
collectEntrypoints(entrypoints, entrypointFiles, packageDir, packageJson.openclaw?.extensions);
|
|
442
|
+
collectEntrypoints(entrypoints, entrypointFiles, packageDir, packageJson.openclaw?.runtimeExtensions);
|
|
433
443
|
} catch {
|
|
434
444
|
errors.push(`${relativePath}: invalid JSON`);
|
|
435
445
|
}
|
|
@@ -439,13 +449,58 @@ async function readPackageMetadata(config, checkoutPath, sourceRoot) {
|
|
|
439
449
|
files: files.sort(),
|
|
440
450
|
errors,
|
|
441
451
|
entrypoints: [...entrypoints].sort(),
|
|
452
|
+
entrypointFiles: [...entrypointFiles].sort(),
|
|
442
453
|
};
|
|
443
454
|
}
|
|
444
455
|
|
|
445
|
-
function
|
|
456
|
+
function collectEntrypoints(entrypoints, entrypointFiles, packageDir, values) {
|
|
457
|
+
if (!Array.isArray(values)) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
for (const value of values) {
|
|
461
|
+
collectEntrypoint(entrypoints, entrypointFiles, packageDir, value);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function collectEntrypoint(entrypoints, entrypointFiles, packageDir, value) {
|
|
446
466
|
if (typeof value === "string" && value.length > 0) {
|
|
447
467
|
entrypoints.add(value);
|
|
468
|
+
for (const candidate of entrypointCandidates(packageDir, value)) {
|
|
469
|
+
if (existsSync(candidate) && isSourceFile(path.basename(candidate), candidate.split(path.sep).join("/"))) {
|
|
470
|
+
entrypointFiles.add(candidate);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function entrypointCandidates(packageDir, specifier) {
|
|
478
|
+
const resolved = path.resolve(packageDir, specifier);
|
|
479
|
+
if (path.extname(resolved)) {
|
|
480
|
+
return [resolved];
|
|
481
|
+
}
|
|
482
|
+
return [
|
|
483
|
+
resolved,
|
|
484
|
+
`${resolved}.js`,
|
|
485
|
+
`${resolved}.mjs`,
|
|
486
|
+
`${resolved}.cjs`,
|
|
487
|
+
`${resolved}.ts`,
|
|
488
|
+
path.join(resolved, "index.js"),
|
|
489
|
+
path.join(resolved, "index.mjs"),
|
|
490
|
+
path.join(resolved, "index.cjs"),
|
|
491
|
+
path.join(resolved, "index.ts"),
|
|
492
|
+
];
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function uniquePaths(paths) {
|
|
496
|
+
return [...new Set(paths)];
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function shouldScanBuildArtifacts(fixture, packageInspection) {
|
|
500
|
+
if (fixture.package) {
|
|
501
|
+
return true;
|
|
448
502
|
}
|
|
503
|
+
return packageInspection.entrypoints.some((entrypoint) => /(^|\/)(?:dist|build)\//.test(entrypoint));
|
|
449
504
|
}
|
|
450
505
|
|
|
451
506
|
async function listSourceFiles(root, options = {}) {
|
|
@@ -484,7 +539,7 @@ function shouldSkipDir(name, normalizedPath, options = {}) {
|
|
|
484
539
|
return (
|
|
485
540
|
name === "node_modules" ||
|
|
486
541
|
(!options.includeDist && name === "dist") ||
|
|
487
|
-
name === "build" ||
|
|
542
|
+
(!options.includeBuild && name === "build") ||
|
|
488
543
|
name === "coverage" ||
|
|
489
544
|
name === ".git" ||
|
|
490
545
|
name === "test" ||
|
package/src/issues.js
CHANGED
|
@@ -42,13 +42,17 @@ export const knownIssueCodes = new Set([
|
|
|
42
42
|
"reserved-sdk-import",
|
|
43
43
|
"security-manifest-schema-unavailable",
|
|
44
44
|
"sdk-load-session-store",
|
|
45
|
+
"sdk-session-file-helper",
|
|
46
|
+
"sdk-session-store-write",
|
|
47
|
+
"sdk-session-transcript-file-target",
|
|
48
|
+
"sdk-session-transcript-low-level",
|
|
45
49
|
"sdk-export-missing",
|
|
46
50
|
"unrecognized-security-manifest",
|
|
47
51
|
]);
|
|
48
52
|
|
|
49
53
|
const authorRemediationDocsUrl = (code) => `https://docs.openclaw.ai/clawhub/plugin-validation-fixes#${code}`;
|
|
50
54
|
|
|
51
|
-
const authorRemediation = (summary) => ({ summary });
|
|
55
|
+
const authorRemediation = (summary, ..._details) => ({ summary });
|
|
52
56
|
|
|
53
57
|
const migrationRemediation = authorRemediation;
|
|
54
58
|
|
|
@@ -124,6 +128,58 @@ export const issueMetadataByCode = {
|
|
|
124
128
|
],
|
|
125
129
|
),
|
|
126
130
|
},
|
|
131
|
+
"sdk-session-store-write": {
|
|
132
|
+
severity: "P2",
|
|
133
|
+
owner: "core",
|
|
134
|
+
decision: "core-compat-adapter",
|
|
135
|
+
title: "deprecated whole-store session write helper is still used",
|
|
136
|
+
authorRemediation: migrationRemediation(
|
|
137
|
+
"Replace deprecated whole-store session writes with row-scoped session helpers.",
|
|
138
|
+
[
|
|
139
|
+
"Use patchSessionEntry(...) when updating fields on an existing session entry.",
|
|
140
|
+
"Use upsertSessionEntry(...) when replacing or creating a session entry.",
|
|
141
|
+
],
|
|
142
|
+
),
|
|
143
|
+
},
|
|
144
|
+
"sdk-session-file-helper": {
|
|
145
|
+
severity: "P2",
|
|
146
|
+
owner: "core",
|
|
147
|
+
decision: "core-compat-adapter",
|
|
148
|
+
title: "deprecated session file-path helper is still used",
|
|
149
|
+
authorRemediation: migrationRemediation(
|
|
150
|
+
"Replace deprecated session file-path helpers with session entry and transcript identity APIs.",
|
|
151
|
+
[
|
|
152
|
+
"Use getSessionEntry(...) to read session metadata by agent/session identity.",
|
|
153
|
+
"Use patchSessionEntry(...) or upsertSessionEntry(...) to persist session metadata.",
|
|
154
|
+
],
|
|
155
|
+
),
|
|
156
|
+
},
|
|
157
|
+
"sdk-session-transcript-file-target": {
|
|
158
|
+
severity: "P2",
|
|
159
|
+
owner: "core",
|
|
160
|
+
decision: "core-compat-adapter",
|
|
161
|
+
title: "deprecated transcript file target helper is still used",
|
|
162
|
+
authorRemediation: migrationRemediation(
|
|
163
|
+
"Replace legacy transcript file targets with public transcript identity or target helpers.",
|
|
164
|
+
[
|
|
165
|
+
"Use resolveSessionTranscriptIdentity(...) when you only need public session identity.",
|
|
166
|
+
"Use resolveSessionTranscriptTarget(...) when you need a structured transcript operation target.",
|
|
167
|
+
],
|
|
168
|
+
),
|
|
169
|
+
},
|
|
170
|
+
"sdk-session-transcript-low-level": {
|
|
171
|
+
severity: "P2",
|
|
172
|
+
owner: "core",
|
|
173
|
+
decision: "core-compat-adapter",
|
|
174
|
+
title: "deprecated low-level transcript helper is still used",
|
|
175
|
+
authorRemediation: migrationRemediation(
|
|
176
|
+
"Replace low-level transcript writes with the structured transcript runtime helpers.",
|
|
177
|
+
[
|
|
178
|
+
"Use appendSessionTranscriptMessageByIdentity(...) for transcript appends.",
|
|
179
|
+
"Use publishSessionTranscriptUpdateByIdentity(...) for transcript update notifications.",
|
|
180
|
+
],
|
|
181
|
+
),
|
|
182
|
+
},
|
|
127
183
|
"sdk-export-missing": {
|
|
128
184
|
severity: "P1",
|
|
129
185
|
owner: "core",
|
|
@@ -462,10 +518,7 @@ export function buildIssues({ breakages = [], warnings = [], suggestions = [], t
|
|
|
462
518
|
runtimeCoverage: finding.runtimeCoverage ?? null,
|
|
463
519
|
...(finding.authorRemediation
|
|
464
520
|
? {
|
|
465
|
-
authorRemediation:
|
|
466
|
-
summary: finding.authorRemediation.summary,
|
|
467
|
-
docsUrl: authorRemediationDocsUrl(finding.code),
|
|
468
|
-
},
|
|
521
|
+
authorRemediation: withAuthorRemediationDocs(finding.code, finding.authorRemediation),
|
|
469
522
|
}
|
|
470
523
|
: {}),
|
|
471
524
|
}));
|
|
@@ -491,10 +544,7 @@ export function issueMetadata(finding, targetOpenClaw) {
|
|
|
491
544
|
};
|
|
492
545
|
const authorMetadata = metadata.authorRemediation
|
|
493
546
|
? {
|
|
494
|
-
authorRemediation:
|
|
495
|
-
summary: metadata.authorRemediation.summary,
|
|
496
|
-
docsUrl: authorRemediationDocsUrl(finding.code),
|
|
497
|
-
},
|
|
547
|
+
authorRemediation: withAuthorRemediationDocs(finding.code, metadata.authorRemediation),
|
|
498
548
|
}
|
|
499
549
|
: {};
|
|
500
550
|
return {
|
|
@@ -505,6 +555,13 @@ export function issueMetadata(finding, targetOpenClaw) {
|
|
|
505
555
|
};
|
|
506
556
|
}
|
|
507
557
|
|
|
558
|
+
function withAuthorRemediationDocs(code, remediation) {
|
|
559
|
+
return {
|
|
560
|
+
...remediation,
|
|
561
|
+
docsUrl: authorRemediationDocsUrl(code),
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
508
565
|
export function classifyIssueFinding(finding, targetOpenClaw, metadata = {}) {
|
|
509
566
|
const compatStatus = compatStatusFor(finding, targetOpenClaw);
|
|
510
567
|
const deprecated = compatStatus === "deprecated";
|
|
@@ -567,6 +624,10 @@ function issueClassFor(code, options) {
|
|
|
567
624
|
"legacy-root-sdk-import",
|
|
568
625
|
"provider-auth-env-vars",
|
|
569
626
|
"sdk-load-session-store",
|
|
627
|
+
"sdk-session-file-helper",
|
|
628
|
+
"sdk-session-store-write",
|
|
629
|
+
"sdk-session-transcript-file-target",
|
|
630
|
+
"sdk-session-transcript-low-level",
|
|
570
631
|
].includes(code)
|
|
571
632
|
) {
|
|
572
633
|
return "deprecation-warning";
|
package/src/json-file.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { lstat, open, readFile, readlink, realpath, rename, stat, unlink } from "node:fs/promises";
|
|
4
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
3
5
|
|
|
4
6
|
export async function readJsonFile(jsonPath) {
|
|
5
7
|
return JSON.parse(await readFile(jsonPath, "utf8"));
|
|
@@ -8,3 +10,98 @@ export async function readJsonFile(jsonPath) {
|
|
|
8
10
|
export async function readOptionalJsonFile(jsonPath) {
|
|
9
11
|
return existsSync(jsonPath) ? readJsonFile(jsonPath) : null;
|
|
10
12
|
}
|
|
13
|
+
|
|
14
|
+
// Writes JSON.stringify(value, null, 2) + newline atomically: stage to a sibling
|
|
15
|
+
// temp file, fsync, then rename over the destination. A crash mid-write can only
|
|
16
|
+
// truncate the temp file, so an interrupted run never corrupts an existing
|
|
17
|
+
// manifest. The destination file mode is preserved exactly (including any
|
|
18
|
+
// special bits) so the atomic replace stays permission-neutral. Existing
|
|
19
|
+
// symlinked manifests resolve to their target before staging so the link itself
|
|
20
|
+
// is never replaced.
|
|
21
|
+
export async function writeJsonFileAtomic(jsonPath, value) {
|
|
22
|
+
const writePath = await resolveJsonWritePath(jsonPath);
|
|
23
|
+
const data = `${JSON.stringify(value, null, 2)}\n`;
|
|
24
|
+
const tempPath = join(dirname(writePath), `.${basename(writePath)}.${process.pid}.${randomUUID()}.tmp`);
|
|
25
|
+
|
|
26
|
+
let existingMode;
|
|
27
|
+
try {
|
|
28
|
+
existingMode = (await stat(writePath)).mode & 0o7777;
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (error.code !== "ENOENT") {
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const handle = await open(tempPath, "wx");
|
|
36
|
+
let stagingError = null;
|
|
37
|
+
try {
|
|
38
|
+
await handle.writeFile(data, "utf8");
|
|
39
|
+
await handle.sync();
|
|
40
|
+
if (existingMode !== undefined) {
|
|
41
|
+
await handle.chmod(existingMode);
|
|
42
|
+
}
|
|
43
|
+
} catch (error) {
|
|
44
|
+
stagingError = error;
|
|
45
|
+
} finally {
|
|
46
|
+
try {
|
|
47
|
+
await handle.close();
|
|
48
|
+
} catch (error) {
|
|
49
|
+
stagingError ??= error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (stagingError) {
|
|
54
|
+
await unlink(tempPath).catch(() => {});
|
|
55
|
+
throw stagingError;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
await rename(tempPath, writePath);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
await unlink(tempPath).catch(() => {});
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function resolveJsonWritePath(jsonPath) {
|
|
67
|
+
try {
|
|
68
|
+
return await realpath(jsonPath);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (error.code !== "ENOENT") {
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let writePath = jsonPath;
|
|
76
|
+
const seen = new Set();
|
|
77
|
+
for (let hops = 0; hops < 40; hops += 1) {
|
|
78
|
+
const normalizedPath = resolve(writePath);
|
|
79
|
+
if (seen.has(normalizedPath)) {
|
|
80
|
+
const error = new Error(`too many symbolic links resolving ${jsonPath}`);
|
|
81
|
+
error.code = "ELOOP";
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
seen.add(normalizedPath);
|
|
85
|
+
|
|
86
|
+
let entry;
|
|
87
|
+
try {
|
|
88
|
+
entry = await lstat(writePath);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (error.code === "ENOENT") {
|
|
91
|
+
return writePath;
|
|
92
|
+
}
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!entry.isSymbolicLink()) {
|
|
97
|
+
return writePath;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const target = await readlink(writePath);
|
|
101
|
+
writePath = resolve(dirname(writePath), target);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const error = new Error(`too many symbolic links resolving ${jsonPath}`);
|
|
105
|
+
error.code = "ELOOP";
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
package/src/profile-diff.js
CHANGED
|
@@ -194,12 +194,16 @@ function registrySurfaceChecks(baseline, current) {
|
|
|
194
194
|
action: "pass",
|
|
195
195
|
metric,
|
|
196
196
|
message: "registry surface delta is tracked as context",
|
|
197
|
-
baseline: baseline.targetOpenClaw[metric],
|
|
198
|
-
current: current.targetOpenClaw[metric],
|
|
199
|
-
delta: current.targetOpenClaw[metric] - baseline.targetOpenClaw[metric],
|
|
197
|
+
baseline: registrySurfaceCount(baseline.targetOpenClaw[metric]),
|
|
198
|
+
current: registrySurfaceCount(current.targetOpenClaw[metric]),
|
|
199
|
+
delta: registrySurfaceCount(current.targetOpenClaw[metric]) - registrySurfaceCount(baseline.targetOpenClaw[metric]),
|
|
200
200
|
}));
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
function registrySurfaceCount(value) {
|
|
204
|
+
return Array.isArray(value) ? value.length : Number(value ?? 0);
|
|
205
|
+
}
|
|
206
|
+
|
|
203
207
|
function commandWall(profile, commandId) {
|
|
204
208
|
return profile.commands.find((command) => command.id === commandId)?.wallMs?.median ?? 0;
|
|
205
209
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFile
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { writeJsonFileAtomic } from "./json-file.js";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
|
|
5
6
|
const packageJsonPath = path.resolve(process.cwd(), "package.json");
|
|
@@ -19,5 +20,5 @@ if (packageJson.devDependencies && Object.keys(packageJson.devDependencies).leng
|
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
if (changed) {
|
|
22
|
-
await
|
|
23
|
+
await writeJsonFileAtomic(packageJsonPath, packageJson);
|
|
23
24
|
}
|
package/src/report.js
CHANGED
|
@@ -1,16 +1,53 @@
|
|
|
1
|
-
const
|
|
1
|
+
const sessionStoreReadReplacement =
|
|
2
2
|
"getSessionEntry(...) / listSessionEntries(...) for reads and patchSessionEntry(...) / upsertSessionEntry(...) for writes";
|
|
3
|
+
const sessionStoreWriteReplacement = "patchSessionEntry(...) / upsertSessionEntry(...) for row-scoped writes";
|
|
4
|
+
const sessionFileReplacement = "session entries and transcript identity helpers instead of persisted file paths";
|
|
5
|
+
const sessionTranscriptReplacement =
|
|
6
|
+
"resolveSessionTranscriptTarget(...), appendSessionTranscriptMessageByIdentity(...), and publishSessionTranscriptUpdateByIdentity(...)";
|
|
3
7
|
|
|
4
|
-
const
|
|
8
|
+
const sdkSessionSpecifiers = new Set([
|
|
5
9
|
"openclaw/plugin-sdk/config-runtime",
|
|
10
|
+
"openclaw/plugin-sdk/mattermost",
|
|
11
|
+
"openclaw/plugin-sdk/agent-harness-runtime",
|
|
6
12
|
"openclaw/plugin-sdk/session-store-runtime",
|
|
13
|
+
"openclaw/plugin-sdk/session-transcript-runtime",
|
|
7
14
|
]);
|
|
8
15
|
|
|
9
16
|
export const pluginSdkDeprecationRules = [
|
|
10
17
|
{
|
|
11
18
|
code: "sdk-load-session-store",
|
|
19
|
+
symbols: new Set(["loadSessionStore"]),
|
|
12
20
|
title: "deprecated whole-store session helper is still used",
|
|
13
|
-
replacement:
|
|
21
|
+
replacement: sessionStoreReadReplacement,
|
|
22
|
+
message: (symbol, replacement) => `${symbol} keeps the legacy whole-store session shape; use ${replacement}.`,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
code: "sdk-session-store-write",
|
|
26
|
+
symbols: new Set(["saveSessionStore", "updateSessionStore"]),
|
|
27
|
+
title: "deprecated whole-store session write helper is still used",
|
|
28
|
+
replacement: sessionStoreWriteReplacement,
|
|
29
|
+
message: (symbol, replacement) => `${symbol} writes the legacy whole-store session shape; use ${replacement}.`,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
code: "sdk-session-file-helper",
|
|
33
|
+
symbols: new Set(["resolveSessionFilePath", "resolveAndPersistSessionFile"]),
|
|
34
|
+
title: "deprecated session file-path helper is still used",
|
|
35
|
+
replacement: sessionFileReplacement,
|
|
36
|
+
message: (symbol, replacement) => `${symbol} depends on legacy session transcript file paths; use ${replacement}.`,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
code: "sdk-session-transcript-file-target",
|
|
40
|
+
symbols: new Set(["resolveSessionTranscriptLegacyFileTarget"]),
|
|
41
|
+
title: "deprecated transcript file target helper is still used",
|
|
42
|
+
replacement: "resolveSessionTranscriptTarget(...) or resolveSessionTranscriptIdentity(...)",
|
|
43
|
+
message: (symbol, replacement) => `${symbol} exposes legacy transcript file targets; use ${replacement}.`,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
code: "sdk-session-transcript-low-level",
|
|
47
|
+
symbols: new Set(["appendSessionTranscriptMessage", "emitSessionTranscriptUpdate"]),
|
|
48
|
+
title: "deprecated low-level transcript helper is still used",
|
|
49
|
+
replacement: sessionTranscriptReplacement,
|
|
50
|
+
message: (symbol, replacement) => `${symbol} bypasses the structured transcript runtime surface; use ${replacement}.`,
|
|
14
51
|
},
|
|
15
52
|
];
|
|
16
53
|
|
|
@@ -18,9 +55,7 @@ export function inspectSdkDeprecations(text, filePath = "source.js", rules = plu
|
|
|
18
55
|
const findings = [];
|
|
19
56
|
|
|
20
57
|
for (const rule of rules) {
|
|
21
|
-
|
|
22
|
-
collectLoadSessionStoreDeprecations(findings, { text, filePath, rule });
|
|
23
|
-
}
|
|
58
|
+
collectSdkHelperDeprecations(findings, { text, filePath, rule });
|
|
24
59
|
}
|
|
25
60
|
|
|
26
61
|
return uniqueFindings(findings)
|
|
@@ -28,12 +63,13 @@ export function inspectSdkDeprecations(text, filePath = "source.js", rules = plu
|
|
|
28
63
|
.map(({ offset, ...finding }) => finding);
|
|
29
64
|
}
|
|
30
65
|
|
|
31
|
-
function
|
|
66
|
+
function collectSdkHelperDeprecations(findings, context) {
|
|
32
67
|
collectNamedImportDeprecations(findings, context);
|
|
33
68
|
collectNamedReexportDeprecations(findings, context);
|
|
34
69
|
collectNamedRequireDeprecations(findings, context);
|
|
35
70
|
collectNamespaceUsageDeprecations(findings, context);
|
|
36
71
|
collectNamespaceRequireDeprecations(findings, context);
|
|
72
|
+
collectDynamicImportNamespaceDeprecations(findings, context);
|
|
37
73
|
collectRuntimeUsageDeprecations(findings, context);
|
|
38
74
|
collectRuntimeAliasUsageDeprecations(findings, context);
|
|
39
75
|
}
|
|
@@ -43,16 +79,17 @@ function collectNamedImportDeprecations(findings, context) {
|
|
|
43
79
|
/\bimport\s+(?:type\s+)?(?:[A-Za-z_$][\w$]*\s*,\s*)?{([^}]+)}\s*from\s*["'`]([^"'`]+)["'`]/g;
|
|
44
80
|
for (const match of context.text.matchAll(regex)) {
|
|
45
81
|
const specifier = match[2];
|
|
46
|
-
if (!
|
|
82
|
+
if (!sdkSessionSpecifiers.has(specifier)) {
|
|
47
83
|
continue;
|
|
48
84
|
}
|
|
49
85
|
for (const binding of parseNamedBindings(match[1])) {
|
|
50
|
-
if (binding.exported
|
|
86
|
+
if (!context.rule.symbols.has(binding.exported)) {
|
|
51
87
|
continue;
|
|
52
88
|
}
|
|
53
89
|
findings.push(
|
|
54
90
|
buildFinding(context.rule, {
|
|
55
|
-
surface: `${specifier} import`,
|
|
91
|
+
surface: `${specifier} ${binding.exported} import`,
|
|
92
|
+
symbol: binding.exported,
|
|
56
93
|
sourceText: context.text,
|
|
57
94
|
filePath: context.filePath,
|
|
58
95
|
offset: (match.index ?? 0) + match[0].lastIndexOf(binding.local),
|
|
@@ -66,16 +103,17 @@ function collectNamedReexportDeprecations(findings, context) {
|
|
|
66
103
|
const regex = /\bexport\s*{([^}]+)}\s*from\s*["'`]([^"'`]+)["'`]/g;
|
|
67
104
|
for (const match of context.text.matchAll(regex)) {
|
|
68
105
|
const specifier = match[2];
|
|
69
|
-
if (!
|
|
106
|
+
if (!sdkSessionSpecifiers.has(specifier)) {
|
|
70
107
|
continue;
|
|
71
108
|
}
|
|
72
109
|
for (const binding of parseNamedBindings(match[1])) {
|
|
73
|
-
if (binding.exported
|
|
110
|
+
if (!context.rule.symbols.has(binding.exported)) {
|
|
74
111
|
continue;
|
|
75
112
|
}
|
|
76
113
|
findings.push(
|
|
77
114
|
buildFinding(context.rule, {
|
|
78
|
-
surface: `${specifier} re-export`,
|
|
115
|
+
surface: `${specifier} ${binding.exported} re-export`,
|
|
116
|
+
symbol: binding.exported,
|
|
79
117
|
sourceText: context.text,
|
|
80
118
|
filePath: context.filePath,
|
|
81
119
|
offset: (match.index ?? 0) + match[0].lastIndexOf(binding.local),
|
|
@@ -89,16 +127,17 @@ function collectNamedRequireDeprecations(findings, context) {
|
|
|
89
127
|
const regex = /\b(?:const|let|var)\s+{([^}]+)}\s*=\s*require\(\s*["'`]([^"'`]+)["'`]\s*\)/g;
|
|
90
128
|
for (const match of context.text.matchAll(regex)) {
|
|
91
129
|
const specifier = match[2];
|
|
92
|
-
if (!
|
|
130
|
+
if (!sdkSessionSpecifiers.has(specifier)) {
|
|
93
131
|
continue;
|
|
94
132
|
}
|
|
95
133
|
for (const binding of parseNamedBindings(match[1], { aliasSeparator: ":" })) {
|
|
96
|
-
if (binding.exported
|
|
134
|
+
if (!context.rule.symbols.has(binding.exported)) {
|
|
97
135
|
continue;
|
|
98
136
|
}
|
|
99
137
|
findings.push(
|
|
100
138
|
buildFinding(context.rule, {
|
|
101
|
-
surface: `${specifier} require`,
|
|
139
|
+
surface: `${specifier} ${binding.exported} require`,
|
|
140
|
+
symbol: binding.exported,
|
|
102
141
|
sourceText: context.text,
|
|
103
142
|
filePath: context.filePath,
|
|
104
143
|
offset: (match.index ?? 0) + match[0].lastIndexOf(binding.local),
|
|
@@ -109,21 +148,24 @@ function collectNamedRequireDeprecations(findings, context) {
|
|
|
109
148
|
}
|
|
110
149
|
|
|
111
150
|
function collectMemberCallDeprecations(findings, context, options) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
151
|
+
for (const symbol of context.rule.symbols) {
|
|
152
|
+
forEachMethodCall(context.text, symbol, (offset) => {
|
|
153
|
+
// Normalize transparent parentheses and optional-chained member links before matching.
|
|
154
|
+
const receiver = readNormalizedCallReceiver(context.text, offset);
|
|
155
|
+
if (!receiver || !options.receiverMatcher(receiver)) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
findings.push(
|
|
159
|
+
buildFinding(context.rule, {
|
|
160
|
+
surface: `${options.surface} ${symbol}`,
|
|
161
|
+
symbol,
|
|
162
|
+
sourceText: context.text,
|
|
163
|
+
filePath: context.filePath,
|
|
164
|
+
offset,
|
|
165
|
+
}),
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
127
169
|
}
|
|
128
170
|
|
|
129
171
|
function forEachMethodCall(text, methodName, visit) {
|
|
@@ -259,7 +301,11 @@ function isIdentifierBoundary(text, offset) {
|
|
|
259
301
|
}
|
|
260
302
|
|
|
261
303
|
function isRuntimeSessionReceiver(receiver) {
|
|
262
|
-
return
|
|
304
|
+
return (
|
|
305
|
+
/(?:^|\.)(?:[A-Za-z_$][A-Za-z0-9_$]*|this)\.runtime\.agent\.session$/.test(receiver) ||
|
|
306
|
+
/^(?:runtime|[A-Za-z_$][A-Za-z0-9_$]*Runtime)\.agent\.session$/.test(receiver) ||
|
|
307
|
+
/^(?:agentRuntime|[A-Za-z_$][A-Za-z0-9_$]*AgentRuntime)\.session$/.test(receiver)
|
|
308
|
+
);
|
|
263
309
|
}
|
|
264
310
|
|
|
265
311
|
function collectNamespaceUsageDeprecations(findings, context) {
|
|
@@ -267,7 +313,7 @@ function collectNamespaceUsageDeprecations(findings, context) {
|
|
|
267
313
|
for (const match of context.text.matchAll(regex)) {
|
|
268
314
|
const local = match[1];
|
|
269
315
|
const specifier = match[2];
|
|
270
|
-
if (!
|
|
316
|
+
if (!sdkSessionSpecifiers.has(specifier)) {
|
|
271
317
|
continue;
|
|
272
318
|
}
|
|
273
319
|
collectMemberCallDeprecations(findings, context, {
|
|
@@ -282,7 +328,7 @@ function collectNamespaceRequireDeprecations(findings, context) {
|
|
|
282
328
|
for (const match of context.text.matchAll(regex)) {
|
|
283
329
|
const local = match[1];
|
|
284
330
|
const specifier = match[2];
|
|
285
|
-
if (!
|
|
331
|
+
if (!sdkSessionSpecifiers.has(specifier)) {
|
|
286
332
|
continue;
|
|
287
333
|
}
|
|
288
334
|
collectMemberCallDeprecations(findings, context, {
|
|
@@ -292,6 +338,22 @@ function collectNamespaceRequireDeprecations(findings, context) {
|
|
|
292
338
|
}
|
|
293
339
|
}
|
|
294
340
|
|
|
341
|
+
function collectDynamicImportNamespaceDeprecations(findings, context) {
|
|
342
|
+
const regex =
|
|
343
|
+
/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:await\s+)?import\(\s*["'`]([^"'`]+)["'`]\s*\)/g;
|
|
344
|
+
for (const match of context.text.matchAll(regex)) {
|
|
345
|
+
const local = match[1];
|
|
346
|
+
const specifier = match[2];
|
|
347
|
+
if (!sdkSessionSpecifiers.has(specifier)) {
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
collectMemberCallDeprecations(findings, context, {
|
|
351
|
+
receiverMatcher: (receiver) => receiver === local,
|
|
352
|
+
surface: `${specifier} dynamic import namespace access`,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
295
357
|
function collectRuntimeUsageDeprecations(findings, context) {
|
|
296
358
|
collectMemberCallDeprecations(findings, context, {
|
|
297
359
|
receiverMatcher: isRuntimeSessionReceiver,
|
|
@@ -509,10 +571,11 @@ function buildFinding(rule, details) {
|
|
|
509
571
|
const refLine = lineForOffset(details.sourceText, details.offset);
|
|
510
572
|
return {
|
|
511
573
|
code: rule.code,
|
|
574
|
+
symbol: details.symbol,
|
|
512
575
|
surface: details.surface,
|
|
513
576
|
replacement: rule.replacement,
|
|
514
577
|
ref: `${details.filePath}:${refLine}`,
|
|
515
|
-
message:
|
|
578
|
+
message: rule.message(details.symbol, rule.replacement),
|
|
516
579
|
offset: details.offset,
|
|
517
580
|
};
|
|
518
581
|
}
|
|
@@ -520,7 +583,7 @@ function buildFinding(rule, details) {
|
|
|
520
583
|
function uniqueFindings(findings) {
|
|
521
584
|
const byKey = new Map();
|
|
522
585
|
for (const finding of findings) {
|
|
523
|
-
byKey.set(`${finding.code}:${finding.surface}:${finding.ref}`, finding);
|
|
586
|
+
byKey.set(`${finding.code}:${finding.symbol}:${finding.surface}:${finding.ref}`, finding);
|
|
524
587
|
}
|
|
525
588
|
return [...byKey.values()];
|
|
526
589
|
}
|
package/src/sdk-mock.js
CHANGED
|
@@ -273,7 +273,10 @@ export async function createMockSdkPackage(rootDir, options = {}) {
|
|
|
273
273
|
if (specifier === "openclaw/plugin-sdk") {
|
|
274
274
|
continue;
|
|
275
275
|
}
|
|
276
|
-
const relative = specifier.slice("openclaw/plugin-sdk/".length);
|
|
276
|
+
const relative = safePluginSdkSubpath(specifier.slice("openclaw/plugin-sdk/".length));
|
|
277
|
+
if (!relative) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
277
280
|
if (mockSdkSubpathExports[relative]) {
|
|
278
281
|
continue;
|
|
279
282
|
}
|
|
@@ -443,7 +446,12 @@ export async function resolve(specifier, context, nextResolve) {
|
|
|
443
446
|
return moduleUrl(path.join(pluginSdkDir, "index.js"));
|
|
444
447
|
}
|
|
445
448
|
if (specifier.startsWith("openclaw/plugin-sdk/")) {
|
|
446
|
-
const subpath = specifier.slice("openclaw/plugin-sdk/".length);
|
|
449
|
+
const subpath = safePluginSdkSubpath(specifier.slice("openclaw/plugin-sdk/".length));
|
|
450
|
+
if (!subpath) {
|
|
451
|
+
throw Object.assign(new Error(\`invalid OpenClaw plugin SDK subpath: \${specifier}\`), {
|
|
452
|
+
code: "ERR_INVALID_MODULE_SPECIFIER",
|
|
453
|
+
});
|
|
454
|
+
}
|
|
447
455
|
return moduleUrl(path.join(pluginSdkDir, \`\${subpath}.js\`));
|
|
448
456
|
}
|
|
449
457
|
if (externalMap.has(specifier)) {
|
|
@@ -514,9 +522,25 @@ function isMockableBareSpecifier(specifier) {
|
|
|
514
522
|
!specifier.startsWith("data:") &&
|
|
515
523
|
!specifier.startsWith("file:");
|
|
516
524
|
}
|
|
525
|
+
|
|
526
|
+
function safePluginSdkSubpath(value) {
|
|
527
|
+
const normalized = path.posix.normalize(String(value).replaceAll("\\\\", "/"));
|
|
528
|
+
if (!normalized || normalized === "." || normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/")) {
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
return normalized;
|
|
532
|
+
}
|
|
517
533
|
`;
|
|
518
534
|
}
|
|
519
535
|
|
|
536
|
+
function safePluginSdkSubpath(value) {
|
|
537
|
+
const normalized = path.posix.normalize(String(value).replaceAll("\\", "/"));
|
|
538
|
+
if (!normalized || normalized === "." || normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/")) {
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
return normalized;
|
|
542
|
+
}
|
|
543
|
+
|
|
520
544
|
function dynamicMockModuleSource(exportNames, options = {}) {
|
|
521
545
|
const names = new Set([...exportNames].filter(isValidExportName));
|
|
522
546
|
if (options.zod) {
|
|
@@ -530,6 +554,9 @@ export default ${options.zod ? "createZNamespace()" : 'createMockValue("default"
|
|
|
530
554
|
}
|
|
531
555
|
|
|
532
556
|
function externalMockModuleSource(specifier, exportNames) {
|
|
557
|
+
if (specifier === "@larksuiteoapi/node-sdk") {
|
|
558
|
+
return larkSdkMockModuleSource(exportNames);
|
|
559
|
+
}
|
|
533
560
|
const names = new Set([...exportNames].filter(isValidExportName));
|
|
534
561
|
if (specifier === "zod") {
|
|
535
562
|
addZodExports(names);
|
|
@@ -537,6 +564,52 @@ function externalMockModuleSource(specifier, exportNames) {
|
|
|
537
564
|
return dynamicMockModuleSource(names, { zod: specifier === "zod" });
|
|
538
565
|
}
|
|
539
566
|
|
|
567
|
+
function larkSdkMockModuleSource(exportNames) {
|
|
568
|
+
const larkExports = new Set([
|
|
569
|
+
"AppType",
|
|
570
|
+
"Client",
|
|
571
|
+
"Domain",
|
|
572
|
+
"EventDispatcher",
|
|
573
|
+
"LoggerLevel",
|
|
574
|
+
"WSClient",
|
|
575
|
+
...exportNames,
|
|
576
|
+
]);
|
|
577
|
+
larkExports.delete("defaultHttpInstance");
|
|
578
|
+
return `${genericMockRuntimeSource()}
|
|
579
|
+
const requestInterceptors = {
|
|
580
|
+
handlers: [],
|
|
581
|
+
use(handler) {
|
|
582
|
+
this.handlers.push(handler);
|
|
583
|
+
return this.handlers.length - 1;
|
|
584
|
+
},
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
export const defaultHttpInstance = {
|
|
588
|
+
interceptors: {
|
|
589
|
+
request: requestInterceptors,
|
|
590
|
+
response: {
|
|
591
|
+
handlers: [],
|
|
592
|
+
use(handler) {
|
|
593
|
+
this.handlers.push(handler);
|
|
594
|
+
return this.handlers.length - 1;
|
|
595
|
+
},
|
|
596
|
+
},
|
|
597
|
+
},
|
|
598
|
+
request: createMockValue("defaultHttpInstance.request"),
|
|
599
|
+
get: createMockValue("defaultHttpInstance.get"),
|
|
600
|
+
post: createMockValue("defaultHttpInstance.post"),
|
|
601
|
+
put: createMockValue("defaultHttpInstance.put"),
|
|
602
|
+
patch: createMockValue("defaultHttpInstance.patch"),
|
|
603
|
+
delete: createMockValue("defaultHttpInstance.delete"),
|
|
604
|
+
head: createMockValue("defaultHttpInstance.head"),
|
|
605
|
+
options: createMockValue("defaultHttpInstance.options"),
|
|
606
|
+
};
|
|
607
|
+
${[...larkExports].filter(isValidExportName).map(genericExportStatement).join("\n")}
|
|
608
|
+
|
|
609
|
+
export default createMockValue("default");
|
|
610
|
+
`;
|
|
611
|
+
}
|
|
612
|
+
|
|
540
613
|
function addZodExports(names) {
|
|
541
614
|
for (const name of ["z", "any", "array", "boolean", "enum", "literal", "number", "object", "record", "string", "unknown"]) {
|
|
542
615
|
names.add(name);
|
package/src/workspace-plan.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { renderPaddedMarkdownTable, writeJsonMarkdownArtifacts } from "./artifacts.js";
|
|
@@ -229,13 +229,13 @@ async function buildEntrypointPlan({ fixtureId, entrypoint, packageSummary, pack
|
|
|
229
229
|
|
|
230
230
|
steps.push({
|
|
231
231
|
kind: "prepare",
|
|
232
|
-
command: `mkdir -p ${workspacePath} && rsync -a --delete ${packageDir}
|
|
232
|
+
command: `mkdir -p ${shellQuote(workspacePath)} && rsync -a --delete ${shellQuote(`${packageDir}/`)} ${shellQuote(`${workspacePath}/`)}`,
|
|
233
233
|
cwd: repoRelative("."),
|
|
234
234
|
reason: "copy fixture package into an isolated mutable workspace",
|
|
235
235
|
});
|
|
236
236
|
steps.push({
|
|
237
237
|
kind: "prepare-artifacts",
|
|
238
|
-
command: `mkdir -p ${resultPath}`,
|
|
238
|
+
command: `mkdir -p ${shellQuote(resultPath)}`,
|
|
239
239
|
cwd: repoRelative("."),
|
|
240
240
|
reason: "create a stable result directory for capture and synthetic probe artifacts",
|
|
241
241
|
});
|
|
@@ -243,7 +243,7 @@ async function buildEntrypointPlan({ fixtureId, entrypoint, packageSummary, pack
|
|
|
243
243
|
if (requiredCapabilities.includes("target-openclaw-link")) {
|
|
244
244
|
steps.push({
|
|
245
245
|
kind: "link-openclaw",
|
|
246
|
-
command: `${packageManager} pkg set dependencies.openclaw=
|
|
246
|
+
command: `${shellQuote(packageManager)} pkg set ${shellQuote(`dependencies.openclaw=file:${targetOpenClawWorkspacePath(settings, fixtureId, targetOpenClawPath)}`)}`,
|
|
247
247
|
cwd: workspacePath,
|
|
248
248
|
reason: "link the plugin's openclaw peer dependency to the target checkout under test",
|
|
249
249
|
});
|
|
@@ -253,7 +253,7 @@ async function buildEntrypointPlan({ fixtureId, entrypoint, packageSummary, pack
|
|
|
253
253
|
if (hasWorkspaceProtocolDevDependencies(packageJson)) {
|
|
254
254
|
steps.push({
|
|
255
255
|
kind: "prune-dev-workspace-deps",
|
|
256
|
-
command: `node ${helperScript(settings, workspacePath, settings.pruneWorkspaceDevDepsScript, "prune-workspace-dev-deps-cli.js")}`,
|
|
256
|
+
command: `node ${shellQuote(helperScript(settings, workspacePath, settings.pruneWorkspaceDevDepsScript, "prune-workspace-dev-deps-cli.js"))}`,
|
|
257
257
|
cwd: workspacePath,
|
|
258
258
|
reason: "remove workspace: devDependencies from the isolated runtime install; the mock SDK supplies OpenClaw host imports",
|
|
259
259
|
});
|
|
@@ -361,6 +361,7 @@ function requiredCapabilitiesFor(entrypoint, packageSummary = {}) {
|
|
|
361
361
|
}
|
|
362
362
|
if (blocker.code === "build-required") {
|
|
363
363
|
capabilities.add("build");
|
|
364
|
+
capabilities.add("dependency-install");
|
|
364
365
|
}
|
|
365
366
|
if (blocker.code === "ts-loader-required") {
|
|
366
367
|
capabilities.add("ts-loader");
|
|
@@ -453,35 +454,35 @@ function installCommand(packageManager) {
|
|
|
453
454
|
function auditCommand(settings, packageManager, fixtureId, workspacePath) {
|
|
454
455
|
const output = workspaceRelativeArtifactPath(settings, fixtureId, workspacePath, "package-audit.json");
|
|
455
456
|
if (packageManager === "npm") {
|
|
456
|
-
return `npm audit --json > ${output} || true`;
|
|
457
|
+
return `npm audit --json > ${shellQuote(output)} || true`;
|
|
457
458
|
}
|
|
458
459
|
if (packageManager === "pnpm") {
|
|
459
|
-
return `pnpm audit --json > ${output} || true`;
|
|
460
|
+
return `pnpm audit --json > ${shellQuote(output)} || true`;
|
|
460
461
|
}
|
|
461
462
|
if (packageManager === "yarn") {
|
|
462
|
-
return `yarn npm audit --json > ${output} || true`;
|
|
463
|
+
return `yarn npm audit --json > ${shellQuote(output)} || true`;
|
|
463
464
|
}
|
|
464
465
|
if (packageManager === "bun") {
|
|
465
|
-
return `bun audit --json > ${output} || true`;
|
|
466
|
+
return `bun audit --json > ${shellQuote(output)} || true`;
|
|
466
467
|
}
|
|
467
|
-
return `${packageManager} audit --json > ${output} || true`;
|
|
468
|
+
return `${shellQuote(packageManager)} audit --json > ${shellQuote(output)} || true`;
|
|
468
469
|
}
|
|
469
470
|
|
|
470
471
|
function runCommand(packageManager, script) {
|
|
471
472
|
if (packageManager === "npm") {
|
|
472
|
-
return `npm run ${script}`;
|
|
473
|
+
return `npm run ${shellQuote(script)}`;
|
|
473
474
|
}
|
|
474
|
-
return `${packageManager} run ${script}`;
|
|
475
|
+
return `${shellQuote(packageManager)} run ${shellQuote(script)}`;
|
|
475
476
|
}
|
|
476
477
|
|
|
477
478
|
function captureCommand(settings, fixtureId, entrypoint, workspacePath) {
|
|
478
479
|
const script = helperScript(settings, workspacePath, settings.captureScript, "capture-cli.js");
|
|
479
|
-
return `${settings.optInEnv} node ${script} ${entrypoint.specifier} --mock-sdk --output ${workspaceArtifactPath(settings, fixtureId, entrypoint, workspacePath, "capture")}`;
|
|
480
|
+
return `${settings.optInEnv} node ${shellQuote(script)} ${shellQuote(entrypoint.specifier)} --mock-sdk --output ${shellQuote(workspaceArtifactPath(settings, fixtureId, entrypoint, workspacePath, "capture"))}`;
|
|
480
481
|
}
|
|
481
482
|
|
|
482
483
|
function syntheticProbeCommand(settings, fixtureId, entrypoint, workspacePath) {
|
|
483
484
|
const script = helperScript(settings, workspacePath, settings.syntheticProbeScript, "synthetic-probes-cli.js");
|
|
484
|
-
return `${settings.optInEnv} node ${script} --entrypoint ${entrypoint.specifier} --mock-sdk --output ${workspaceArtifactPath(settings, fixtureId, entrypoint, workspacePath, "synthetic")}`;
|
|
485
|
+
return `${settings.optInEnv} node ${shellQuote(script)} --entrypoint ${shellQuote(entrypoint.specifier)} --mock-sdk --output ${shellQuote(workspaceArtifactPath(settings, fixtureId, entrypoint, workspacePath, "synthetic"))}`;
|
|
485
486
|
}
|
|
486
487
|
|
|
487
488
|
function helperScript(settings, workspacePath, configuredScript, helperFileName) {
|
|
@@ -526,6 +527,14 @@ function auditArtifactPath(settings, fixtureId) {
|
|
|
526
527
|
return posixJoin(settings.resultsRoot, fixtureId, "package-audit.json");
|
|
527
528
|
}
|
|
528
529
|
|
|
530
|
+
function shellQuote(value) {
|
|
531
|
+
const text = String(value);
|
|
532
|
+
if (/^[A-Za-z0-9_./:=@%+-]+$/u.test(text)) {
|
|
533
|
+
return text;
|
|
534
|
+
}
|
|
535
|
+
return `'${text.replaceAll("'", "'\\''")}'`;
|
|
536
|
+
}
|
|
537
|
+
|
|
529
538
|
function markdownTable(rows, headers) {
|
|
530
539
|
return renderPaddedMarkdownTable(rows, headers);
|
|
531
540
|
}
|