
























@@ -8,6 +8,7 @@ import {
88unscopedPackageName,
99} from "../infra/install-safe-path.js";
1010import { type NpmIntegrityDrift, type NpmSpecResolution } from "../infra/install-source-utils.js";
11+import { resolveOpenClawPackageRootSync } from "../infra/openclaw-root.js";
1112import { normalizeOptionalString } from "../shared/string-coerce.js";
1213import { CONFIG_DIR, resolveUserPath } from "../utils.js";
1314import type { InstallSecurityScanResult } from "./install-security-scan.js";
@@ -31,6 +32,7 @@ type PluginInstallLogger = {
31323233type PackageManifest = PluginPackageManifest & {
3334dependencies?: Record<string, string>;
35+peerDependencies?: Record<string, string>;
3436};
35373638const MISSING_EXTENSIONS_ERROR =
@@ -608,6 +610,71 @@ async function detectNativePackageInstallSource(packageDir: string): Promise<boo
608610}
609611}
610612613+/**
614+ * After npm install completes, symlink any peerDependencies that name the host
615+ * openclaw package into the plugin's node_modules/ directory. npm never
616+ * materialises peerDependencies automatically, so plugins that moved openclaw
617+ * from dependencies → peerDependencies would fail at runtime without this.
618+ */
619+async function linkOpenClawPeerDependencies(params: {
620+installedDir: string;
621+peerDependencies: Record<string, string>;
622+logger: PluginInstallLogger;
623+}): Promise<void> {
624+const OPENCLAW_PEER_NAMES = new Set(["openclaw"]);
625+const peers = Object.keys(params.peerDependencies).filter((name) =>
626+OPENCLAW_PEER_NAMES.has(name),
627+);
628+if (peers.length === 0) {
629+return;
630+}
631+632+const hostRoot = resolveOpenClawPackageRootSync({
633+argv1: process.argv[1],
634+moduleUrl: import.meta.url,
635+cwd: process.cwd(),
636+});
637+if (!hostRoot) {
638+params.logger.warn?.(
639+"Could not locate openclaw package root to symlink peerDependencies; plugin may fail to resolve openclaw at runtime.",
640+);
641+return;
642+}
643+644+const nodeModulesDir = path.join(params.installedDir, "node_modules");
645+await fs.mkdir(nodeModulesDir, { recursive: true });
646+647+for (const peerName of peers) {
648+const linkPath = path.join(nodeModulesDir, peerName);
649+// Resolve the actual source for scoped packages (e.g. @scope/name).
650+const linkTarget = path.join(hostRoot, "node_modules", peerName);
651+// Check whether the package exists at the expected location inside the
652+// host root's node_modules. If it does not (e.g. openclaw IS the root
653+// package), fall back to the host root itself.
654+let resolvedTarget: string;
655+try {
656+await fs.access(path.join(linkTarget, "package.json"));
657+resolvedTarget = linkTarget;
658+} catch {
659+resolvedTarget = hostRoot;
660+}
661+662+try {
663+// Remove any existing entry (broken link or stale directory) before
664+// creating the new symlink so re-installs are idempotent.
665+await fs.rm(linkPath, { recursive: true, force: true });
666+await fs.symlink(resolvedTarget, linkPath, "junction");
667+params.logger.info?.(
668+`Linked peerDependency "${peerName}" → ${resolvedTarget}`,
669+);
670+} catch (err) {
671+params.logger.warn?.(
672+`Failed to symlink peerDependency "${peerName}": ${String(err)}`,
673+);
674+}
675+}
676+}
677+611678async function installPluginFromPackageDir(
612679params: {
613680packageDir: string;
@@ -742,6 +809,7 @@ async function installPluginFromPackageDir(
742809}
743810744811const deps = manifest.dependencies ?? {};
812+const peerDeps = manifest.peerDependencies ?? {};
745813return await installPluginDirectoryIntoExtensions({
746814sourceDir: params.packageDir,
747815 pluginId,
@@ -755,7 +823,7 @@ async function installPluginFromPackageDir(
755823mode: targetResult.target.effectiveMode,
756824 dryRun,
757825copyErrorPrefix: "failed to copy plugin",
758-hasDeps: Object.keys(deps).length > 0,
826+hasDeps: Object.keys(deps).length > 0 || Object.keys(peerDeps).length > 0,
759827depsLogMessage: "Installing plugin dependencies…",
760828nameEncoder: encodePluginInstallDirName,
761829afterCopy: async (installedDir) => {
@@ -770,16 +838,25 @@ async function installPluginFromPackageDir(
770838}
771839}
772840},
773-afterInstall: async (installedDir) =>
774-await runInstallSourceScan({
841+afterInstall: async (installedDir) => {
842+// Symlink any openclaw peerDependencies into the plugin's node_modules/
843+// so that plugins declaring openclaw as a peerDependency (rather than a
844+// regular dependency) can resolve it at runtime.
845+await linkOpenClawPeerDependencies({
846+ installedDir,
847+peerDependencies: peerDeps,
848+ logger,
849+});
850+return await runInstallSourceScan({
775851subject: `Plugin "${pluginId}"`,
776852scan: async () =>
777853await runtime.scanInstalledPackageDependencyTree({
778854 logger,
779855packageDir: installedDir,
780856 pluginId,
781857}),
782-}),
858+});
859+},
783860});
784861}
785862此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。