From f0bb00f54ba3fcafd2e988653626020f19db70f4 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 4 May 2026 00:18:40 +0100 Subject: [PATCH] fix: align official plugin repair versions --- .../missing-configured-plugin-install.test.ts | 37 +++++++++++-------- .../missing-configured-plugin-install.ts | 37 +++++++++++++++---- 2 files changed, 50 insertions(+), 24 deletions(-) diff --git a/src/commands/doctor/shared/missing-configured-plugin-install.test.ts b/src/commands/doctor/shared/missing-configured-plugin-install.test.ts index 24277f232a7..ed9ff9857fd 100644 --- a/src/commands/doctor/shared/missing-configured-plugin-install.test.ts +++ b/src/commands/doctor/shared/missing-configured-plugin-install.test.ts @@ -1,4 +1,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; +import { VERSION } from "../../../version.js"; + +const openClawReleaseSpec = (packageName: string) => `${packageName}@${VERSION}`; const mocks = vi.hoisted(() => ({ installPluginFromClawHub: vi.fn(), @@ -343,13 +346,13 @@ describe("repairMissingConfiguredPluginInstalls", () => { expect(mocks.installPluginFromClawHub).not.toHaveBeenCalled(); expect(mocks.installPluginFromNpmSpec).toHaveBeenCalledWith( expect.objectContaining({ - spec: "@openclaw/twitch", + spec: openClawReleaseSpec("@openclaw/twitch"), expectedPluginId: "twitch", trustedSourceLinkedOfficialInstall: true, }), ); expect(result.changes).toEqual([ - 'Installed missing configured plugin "twitch" from @openclaw/twitch.', + `Installed missing configured plugin "twitch" from ${openClawReleaseSpec("@openclaw/twitch")}.`, ]); }); @@ -395,12 +398,12 @@ describe("repairMissingConfiguredPluginInstalls", () => { expect(mocks.installPluginFromClawHub).not.toHaveBeenCalled(); expect(mocks.installPluginFromNpmSpec).toHaveBeenCalledWith( expect.objectContaining({ - spec: "@openclaw/diagnostics-otel", + spec: openClawReleaseSpec("@openclaw/diagnostics-otel"), expectedPluginId: "diagnostics-otel", }), ); expect(result.changes).toEqual([ - 'Installed missing configured plugin "diagnostics-otel" from @openclaw/diagnostics-otel.', + `Installed missing configured plugin "diagnostics-otel" from ${openClawReleaseSpec("@openclaw/diagnostics-otel")}.`, ]); }); @@ -442,13 +445,13 @@ describe("repairMissingConfiguredPluginInstalls", () => { expect(mocks.installPluginFromNpmSpec).toHaveBeenCalledWith( expect.objectContaining({ - spec: "@openclaw/acpx", + spec: openClawReleaseSpec("@openclaw/acpx"), expectedPluginId: "acpx", trustedSourceLinkedOfficialInstall: true, }), ); expect(result.changes).toEqual([ - 'Installed missing configured plugin "acpx" from @openclaw/acpx.', + `Installed missing configured plugin "acpx" from ${openClawReleaseSpec("@openclaw/acpx")}.`, ]); }); @@ -999,7 +1002,7 @@ describe("repairMissingConfiguredPluginInstalls", () => { expect(mocks.resolveProviderInstallCatalogEntries).toHaveBeenCalled(); expect(mocks.installPluginFromNpmSpec).toHaveBeenCalledWith( expect.objectContaining({ - spec: "@openclaw/codex", + spec: openClawReleaseSpec("@openclaw/codex"), expectedPluginId: "codex", trustedSourceLinkedOfficialInstall: true, }), @@ -1008,7 +1011,7 @@ describe("repairMissingConfiguredPluginInstalls", () => { expect.objectContaining({ codex: expect.objectContaining({ source: "npm", - spec: "@openclaw/codex", + spec: openClawReleaseSpec("@openclaw/codex"), installPath: "/tmp/openclaw-plugins/codex", version: "2026.5.2", }), @@ -1016,7 +1019,7 @@ describe("repairMissingConfiguredPluginInstalls", () => { { env: {} }, ); expect(result.changes).toEqual([ - 'Installed missing configured plugin "codex" from @openclaw/codex.', + `Installed missing configured plugin "codex" from ${openClawReleaseSpec("@openclaw/codex")}.`, ]); expect(result.warnings).toEqual([]); }); @@ -1077,7 +1080,7 @@ describe("repairMissingConfiguredPluginInstalls", () => { expect(mocks.installPluginFromNpmSpec).toHaveBeenCalledWith( expect.objectContaining({ - spec: "@openclaw/codex", + spec: openClawReleaseSpec("@openclaw/codex"), expectedPluginId: "codex", trustedSourceLinkedOfficialInstall: true, }), @@ -1086,7 +1089,7 @@ describe("repairMissingConfiguredPluginInstalls", () => { expect.objectContaining({ codex: expect.objectContaining({ source: "npm", - spec: "@openclaw/codex", + spec: openClawReleaseSpec("@openclaw/codex"), installPath: "/tmp/openclaw-plugins/codex", version: "2026.5.2", }), @@ -1094,7 +1097,9 @@ describe("repairMissingConfiguredPluginInstalls", () => { { env }, ); expect(result).toEqual({ - changes: ['Installed missing configured plugin "codex" from @openclaw/codex.'], + changes: [ + `Installed missing configured plugin "codex" from ${openClawReleaseSpec("@openclaw/codex")}.`, + ], warnings: [], }); }); @@ -1271,7 +1276,7 @@ describe("repairMissingConfiguredPluginInstalls", () => { ); expect(mocks.installPluginFromNpmSpec).toHaveBeenCalledWith( expect.objectContaining({ - spec: "@openclaw/discord", + spec: openClawReleaseSpec("@openclaw/discord"), expectedPluginId: "discord", trustedSourceLinkedOfficialInstall: true, }), @@ -1283,7 +1288,7 @@ describe("repairMissingConfiguredPluginInstalls", () => { { env: {} }, ); expect(result.changes).toEqual([ - 'Installed missing configured plugin "discord" from @openclaw/discord.', + `Installed missing configured plugin "discord" from ${openClawReleaseSpec("@openclaw/discord")}.`, ]); }); @@ -1616,13 +1621,13 @@ describe("repairMissingConfiguredPluginInstalls", () => { expect(mocks.installPluginFromNpmSpec).toHaveBeenCalledWith( expect.objectContaining({ - spec: "@openclaw/brave-plugin", + spec: openClawReleaseSpec("@openclaw/brave-plugin"), expectedPluginId: "brave", trustedSourceLinkedOfficialInstall: true, }), ); expect(result.changes).toEqual([ - 'Installed missing configured plugin "brave" from @openclaw/brave-plugin.', + `Installed missing configured plugin "brave" from ${openClawReleaseSpec("@openclaw/brave-plugin")}.`, ]); }); diff --git a/src/commands/doctor/shared/missing-configured-plugin-install.ts b/src/commands/doctor/shared/missing-configured-plugin-install.ts index 0ff14ba7799..efecd721ea9 100644 --- a/src/commands/doctor/shared/missing-configured-plugin-install.ts +++ b/src/commands/doctor/shared/missing-configured-plugin-install.ts @@ -8,7 +8,7 @@ import { listChannelPluginCatalogEntries } from "../../../channels/plugins/catal import type { OpenClawConfig } from "../../../config/types.openclaw.js"; import type { PluginInstallRecord } from "../../../config/types.plugins.js"; import { parseClawHubPluginSpec } from "../../../infra/clawhub-spec.js"; -import { parseRegistryNpmSpec } from "../../../infra/npm-registry-spec.js"; +import { isExactSemverVersion, parseRegistryNpmSpec } from "../../../infra/npm-registry-spec.js"; import { buildClawHubPluginInstallRecordFields } from "../../../plugins/clawhub-install-records.js"; import { CLAWHUB_INSTALL_ERROR_CODE, installPluginFromClawHub } from "../../../plugins/clawhub.js"; import { resolveDefaultPluginExtensionsDir } from "../../../plugins/install-paths.js"; @@ -30,6 +30,7 @@ import { resolveProviderInstallCatalogEntries } from "../../../plugins/provider- import { updateNpmInstalledPlugins } from "../../../plugins/update.js"; import { resolveWebSearchInstallCatalogEntry } from "../../../plugins/web-search-install-catalog.js"; import { resolveUserPath } from "../../../utils.js"; +import { VERSION } from "../../../version.js"; import { asObjectRecord } from "./object.js"; type DownloadableInstallCandidate = { @@ -381,6 +382,27 @@ function recordClawHubPackageName(value: string | undefined): string | undefined return parseClawHubPluginSpec(trimmed)?.name ?? trimmed; } +function resolveVersionAlignedOfficialNpmSpec( + candidate: DownloadableInstallCandidate, +): string | undefined { + const npmSpec = candidate.npmSpec?.trim(); + if (!npmSpec || !candidate.trustedSourceLinkedOfficialInstall) { + return npmSpec; + } + const parsed = parseRegistryNpmSpec(npmSpec); + if (!parsed || !parsed.name.startsWith("@openclaw/")) { + return npmSpec; + } + if (parsed.selectorKind === "exact-version") { + return npmSpec; + } + const hostVersion = VERSION.trim(); + if (!isExactSemverVersion(hostVersion)) { + return npmSpec; + } + return `${parsed.name}@${hostVersion}`; +} + async function installCandidate(params: { candidate: DownloadableInstallCandidate; records: Record; @@ -430,7 +452,8 @@ async function installCandidate(params: { `ClawHub ${candidate.clawhubSpec} unavailable for "${candidate.pluginId}"; falling back to npm ${candidate.npmSpec}.`, ); } - if (!candidate.npmSpec) { + const npmSpec = resolveVersionAlignedOfficialNpmSpec(candidate); + if (!npmSpec) { return { records: params.records, changes: [], @@ -440,7 +463,7 @@ async function installCandidate(params: { }; } const result = await installPluginFromNpmSpec({ - spec: candidate.npmSpec, + spec: npmSpec, extensionsDir, expectedPluginId: candidate.pluginId, expectedIntegrity: candidate.expectedIntegrity, @@ -464,17 +487,14 @@ async function installCandidate(params: { ...params.records, [pluginId]: { source: "npm", - spec: candidate.npmSpec, + spec: npmSpec, installPath: result.targetDir, version: result.version, installedAt: new Date().toISOString(), ...buildNpmResolutionInstallFields(result.npmResolution), }, }, - changes: [ - ...changes, - `Installed missing configured plugin "${pluginId}" from ${candidate.npmSpec}.`, - ], + changes: [...changes, `Installed missing configured plugin "${pluginId}" from ${npmSpec}.`], warnings: [], }; } @@ -672,4 +692,5 @@ export const __testing = { collectConfiguredChannelIds, collectConfiguredPluginIds, collectDownloadableInstallCandidates, + resolveVersionAlignedOfficialNpmSpec, };