fix(plugins): update external plugins in recorded root

This commit is contained in:
Peter Steinberger
2026-04-26 07:14:21 +01:00
parent 1b99f8aedb
commit 40e5d9adc7
4 changed files with 106 additions and 1 deletions

View File

@@ -746,6 +746,7 @@ export async function installPluginFromClawHub(
token?: string;
logger?: PluginInstallLogger;
mode?: "install" | "update";
extensionsDir?: string;
dryRun?: boolean;
expectedPluginId?: string;
},
@@ -862,6 +863,7 @@ export async function installPluginFromClawHub(
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
logger: params.logger,
mode: params.mode,
extensionsDir: params.extensionsDir,
dryRun: params.dryRun,
expectedPluginId: params.expectedPluginId,
});

View File

@@ -1109,6 +1109,7 @@ export async function installPluginFromMarketplace(
logger?: MarketplaceLogger;
timeoutMs?: number;
mode?: "install" | "update";
extensionsDir?: string;
dryRun?: boolean;
expectedPluginId?: string;
},
@@ -1154,6 +1155,7 @@ export async function installPluginFromMarketplace(
path: resolved.path,
logger: params.logger,
mode: params.mode,
extensionsDir: params.extensionsDir,
dryRun: params.dryRun,
expectedPluginId: params.expectedPluginId,
});

View File

@@ -21,7 +21,8 @@ const tempDirs: string[] = [];
vi.mock("./install.js", () => ({
installPluginFromNpmSpec: (...args: unknown[]) => installPluginFromNpmSpecMock(...args),
resolvePluginInstallDir: (pluginId: string) => `/tmp/${pluginId}`,
resolvePluginInstallDir: (pluginId: string, extensionsDir = "/tmp") =>
`${extensionsDir}/${pluginId}`,
PLUGIN_INSTALL_ERROR_CODE: {
NPM_PACKAGE_NOT_FOUND: "npm_package_not_found",
},
@@ -920,6 +921,82 @@ describe("updateNpmInstalledPlugins", () => {
}),
);
});
it("reuses the recorded managed extensions root when updating external plugins", async () => {
const installPath = "/var/openclaw/extensions/demo";
const extensionsDir = "/var/openclaw/extensions";
installPluginFromNpmSpecMock.mockResolvedValue(
createSuccessfulNpmUpdateResult({
pluginId: "demo",
targetDir: installPath,
version: "1.2.0",
}),
);
installPluginFromClawHubMock.mockResolvedValue({
ok: true,
pluginId: "demo",
targetDir: installPath,
version: "1.2.0",
extensions: ["index.ts"],
clawhub: {
source: "clawhub",
clawhubUrl: "https://clawhub.ai",
clawhubPackage: "demo",
clawhubFamily: "code-plugin",
clawhubChannel: "official",
integrity: "sha256-next",
resolvedAt: "2026-03-22T00:00:00.000Z",
},
});
installPluginFromMarketplaceMock.mockResolvedValue({
ok: true,
pluginId: "demo",
targetDir: installPath,
version: "1.2.0",
extensions: ["index.ts"],
marketplaceSource: "acme/plugins",
marketplacePlugin: "demo",
});
await updateNpmInstalledPlugins({
config: createNpmInstallConfig({
pluginId: "demo",
spec: "@acme/demo",
installPath,
}),
pluginIds: ["demo"],
});
await updateNpmInstalledPlugins({
config: createClawHubInstallConfig({
pluginId: "demo",
installPath,
clawhubUrl: "https://clawhub.ai",
clawhubPackage: "demo",
clawhubFamily: "code-plugin",
clawhubChannel: "official",
}),
pluginIds: ["demo"],
});
await updateNpmInstalledPlugins({
config: createMarketplaceInstallConfig({
pluginId: "demo",
installPath,
marketplaceSource: "acme/plugins",
marketplacePlugin: "demo",
}),
pluginIds: ["demo"],
});
expect(installPluginFromNpmSpecMock).toHaveBeenCalledWith(
expect.objectContaining({ extensionsDir }),
);
expect(installPluginFromClawHubMock).toHaveBeenCalledWith(
expect.objectContaining({ extensionsDir }),
);
expect(installPluginFromMarketplaceMock).toHaveBeenCalledWith(
expect.objectContaining({ extensionsDir }),
);
});
});
describe("syncPluginsForUpdateChannel", () => {

View File

@@ -1,3 +1,4 @@
import path from "node:path";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { PluginInstallRecord } from "../config/types.plugins.js";
import type { NpmSpecResolution } from "../infra/install-source-utils.js";
@@ -155,6 +156,19 @@ function pathsEqual(
return resolveUserPath(left, env) === resolveUserPath(right, env);
}
function resolveRecordedExtensionsDir(params: {
pluginId: string;
installPath: string;
}): string | undefined {
const parentDir = path.dirname(params.installPath);
try {
const canonicalInstallPath = resolvePluginInstallDir(params.pluginId, parentDir);
return canonicalInstallPath === params.installPath ? parentDir : undefined;
} catch {
return undefined;
}
}
function buildLoadPathHelpers(existing: string[], env: NodeJS.ProcessEnv = process.env) {
let paths = [...existing];
const resolveSet = () => new Set(paths.map((entry) => resolveUserPath(entry, env)));
@@ -535,6 +549,10 @@ export async function updateNpmInstalledPlugins(params: {
continue;
}
const currentVersion = await readInstalledPackageVersion(installPath);
const extensionsDir = resolveRecordedExtensionsDir({
pluginId,
installPath,
});
if (!params.dryRun && record.source === "npm" && currentVersion) {
const metadataResult = await resolveNpmSpecMetadata({ spec: effectiveSpec! });
@@ -573,6 +591,7 @@ export async function updateNpmInstalledPlugins(params: {
? await installPluginFromNpmSpec({
spec: effectiveSpec!,
mode: "update",
extensionsDir,
dryRun: true,
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
expectedPluginId: pluginId,
@@ -590,6 +609,7 @@ export async function updateNpmInstalledPlugins(params: {
spec: effectiveSpec ?? `clawhub:${record.clawhubPackage!}`,
baseUrl: record.clawhubUrl,
mode: "update",
extensionsDir,
dryRun: true,
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
expectedPluginId: pluginId,
@@ -599,6 +619,7 @@ export async function updateNpmInstalledPlugins(params: {
marketplace: record.marketplaceSource!,
plugin: record.marketplacePlugin!,
mode: "update",
extensionsDir,
dryRun: true,
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
expectedPluginId: pluginId,
@@ -674,6 +695,7 @@ export async function updateNpmInstalledPlugins(params: {
? await installPluginFromNpmSpec({
spec: effectiveSpec!,
mode: "update",
extensionsDir,
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
expectedPluginId: pluginId,
expectedIntegrity,
@@ -690,6 +712,7 @@ export async function updateNpmInstalledPlugins(params: {
spec: effectiveSpec ?? `clawhub:${record.clawhubPackage!}`,
baseUrl: record.clawhubUrl,
mode: "update",
extensionsDir,
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
expectedPluginId: pluginId,
logger,
@@ -698,6 +721,7 @@ export async function updateNpmInstalledPlugins(params: {
marketplace: record.marketplaceSource!,
plugin: record.marketplacePlugin!,
mode: "update",
extensionsDir,
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
expectedPluginId: pluginId,
logger,