mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:30:42 +00:00
fix(plugins): repair missing openclaw peer links on update
This commit is contained in:
committed by
Peter Steinberger
parent
0eb06caae3
commit
2e8761c5c1
@@ -24,7 +24,11 @@ export function expectedIntegrityForUpdate(
|
||||
return integrity;
|
||||
}
|
||||
|
||||
export async function readInstalledPackageVersion(dir: string): Promise<string | undefined> {
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function readInstalledPackageManifest(dir: string): Record<string, unknown> | undefined {
|
||||
const manifestPath = path.join(dir, "package.json");
|
||||
const opened = openBoundaryFileSync({
|
||||
absolutePath: manifestPath,
|
||||
@@ -35,12 +39,32 @@ export async function readInstalledPackageVersion(dir: string): Promise<string |
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const raw = fsSync.readFileSync(opened.fd, "utf-8");
|
||||
const parsed = JSON.parse(raw) as { version?: unknown };
|
||||
return typeof parsed.version === "string" ? parsed.version : undefined;
|
||||
const parsed = JSON.parse(fsSync.readFileSync(opened.fd, "utf-8")) as unknown;
|
||||
return isRecord(parsed) ? parsed : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
} finally {
|
||||
fsSync.closeSync(opened.fd);
|
||||
}
|
||||
}
|
||||
|
||||
export async function readInstalledPackageVersion(dir: string): Promise<string | undefined> {
|
||||
const manifest = readInstalledPackageManifest(dir);
|
||||
return typeof manifest?.version === "string" ? manifest.version : undefined;
|
||||
}
|
||||
|
||||
export function installedPackageNeedsOpenClawPeerLinkRepair(dir: string): boolean {
|
||||
const manifest = readInstalledPackageManifest(dir);
|
||||
const peerDependencies = isRecord(manifest?.peerDependencies) ? manifest.peerDependencies : {};
|
||||
if (!Object.hasOwn(peerDependencies, "openclaw")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
fsSync.statSync(path.join(dir, "node_modules", "openclaw"));
|
||||
return false;
|
||||
} catch (error) {
|
||||
const code = (error as NodeJS.ErrnoException | undefined)?.code;
|
||||
return code === "ENOENT" || code === "ENOTDIR";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,12 +250,24 @@ function createCodexAppServerInstallConfig(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function createInstalledPackageDir(params: { name?: string; version: string }): string {
|
||||
function createInstalledPackageDir(params: {
|
||||
name?: string;
|
||||
version: string;
|
||||
peerDependencies?: Record<string, string>;
|
||||
}): string {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-plugin-update-test-"));
|
||||
tempDirs.push(dir);
|
||||
fs.writeFileSync(
|
||||
path.join(dir, "package.json"),
|
||||
JSON.stringify({ name: params.name ?? "test-plugin", version: params.version }, null, 2),
|
||||
JSON.stringify(
|
||||
{
|
||||
name: params.name ?? "test-plugin",
|
||||
version: params.version,
|
||||
...(params.peerDependencies ? { peerDependencies: params.peerDependencies } : {}),
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
return dir;
|
||||
}
|
||||
@@ -708,6 +720,119 @@ describe("updateNpmInstalledPlugins", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("repairs missing openclaw peer links before skipping unchanged npm plugins", async () => {
|
||||
const installPath = createInstalledPackageDir({
|
||||
name: "@openclaw/codex",
|
||||
version: "2026.5.3",
|
||||
peerDependencies: { openclaw: ">=2026.5.3" },
|
||||
});
|
||||
mockNpmViewMetadata({
|
||||
name: "@openclaw/codex",
|
||||
version: "2026.5.3",
|
||||
integrity: "sha512-same",
|
||||
shasum: "same",
|
||||
});
|
||||
installPluginFromNpmSpecMock.mockResolvedValue(
|
||||
createSuccessfulNpmUpdateResult({
|
||||
pluginId: "codex",
|
||||
targetDir: installPath,
|
||||
version: "2026.5.3",
|
||||
npmResolution: {
|
||||
name: "@openclaw/codex",
|
||||
version: "2026.5.3",
|
||||
resolvedSpec: "@openclaw/codex@2026.5.3",
|
||||
},
|
||||
}),
|
||||
);
|
||||
const config: OpenClawConfig = {
|
||||
plugins: {
|
||||
installs: {
|
||||
codex: {
|
||||
source: "npm",
|
||||
spec: "@openclaw/codex",
|
||||
installPath,
|
||||
resolvedName: "@openclaw/codex",
|
||||
resolvedVersion: "2026.5.3",
|
||||
resolvedSpec: "@openclaw/codex@2026.5.3",
|
||||
integrity: "sha512-same",
|
||||
shasum: "same",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = await updateNpmInstalledPlugins({
|
||||
config,
|
||||
pluginIds: ["codex"],
|
||||
});
|
||||
|
||||
expect(installPluginFromNpmSpecMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
spec: "@openclaw/codex",
|
||||
mode: "update",
|
||||
expectedPluginId: "codex",
|
||||
}),
|
||||
);
|
||||
expect(result.changed).toBe(true);
|
||||
expect(result.outcomes).toEqual([
|
||||
{
|
||||
pluginId: "codex",
|
||||
status: "unchanged",
|
||||
currentVersion: "2026.5.3",
|
||||
nextVersion: "2026.5.3",
|
||||
message: "codex already at 2026.5.3.",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("skips unchanged npm plugins when the openclaw peer link already resolves", async () => {
|
||||
const installPath = createInstalledPackageDir({
|
||||
name: "@openclaw/codex",
|
||||
version: "2026.5.3",
|
||||
peerDependencies: { openclaw: ">=2026.5.3" },
|
||||
});
|
||||
fs.mkdirSync(path.join(installPath, "node_modules", "openclaw"), { recursive: true });
|
||||
mockNpmViewMetadata({
|
||||
name: "@openclaw/codex",
|
||||
version: "2026.5.3",
|
||||
integrity: "sha512-same",
|
||||
shasum: "same",
|
||||
});
|
||||
installPluginFromNpmSpecMock.mockRejectedValue(new Error("installer should not run"));
|
||||
|
||||
const result = await updateNpmInstalledPlugins({
|
||||
config: {
|
||||
plugins: {
|
||||
installs: {
|
||||
codex: {
|
||||
source: "npm",
|
||||
spec: "@openclaw/codex",
|
||||
installPath,
|
||||
resolvedName: "@openclaw/codex",
|
||||
resolvedVersion: "2026.5.3",
|
||||
resolvedSpec: "@openclaw/codex@2026.5.3",
|
||||
integrity: "sha512-same",
|
||||
shasum: "same",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
pluginIds: ["codex"],
|
||||
});
|
||||
|
||||
expect(installPluginFromNpmSpecMock).not.toHaveBeenCalled();
|
||||
expect(result.changed).toBe(false);
|
||||
expect(result.outcomes).toEqual([
|
||||
{
|
||||
pluginId: "codex",
|
||||
status: "unchanged",
|
||||
currentVersion: "2026.5.3",
|
||||
nextVersion: "2026.5.3",
|
||||
message: "codex is up to date (2026.5.3).",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("refreshes legacy npm install records before skipping unchanged artifacts", async () => {
|
||||
const installPath = createInstalledPackageDir({
|
||||
name: "@martian-engineering/lossless-claw",
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "../infra/npm-registry-spec.js";
|
||||
import {
|
||||
expectedIntegrityForUpdate,
|
||||
installedPackageNeedsOpenClawPeerLinkRepair,
|
||||
readInstalledPackageVersion,
|
||||
} from "../infra/package-update-utils.js";
|
||||
import { compareComparableSemver, parseComparableSemver } from "../infra/semver-compare.js";
|
||||
@@ -989,6 +990,7 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
spec: effectiveSpec!,
|
||||
trustedSourceLinkedOfficialInstall,
|
||||
}) &&
|
||||
!installedPackageNeedsOpenClawPeerLinkRepair(installPath) &&
|
||||
shouldSkipUnchangedNpmInstall({
|
||||
currentVersion,
|
||||
record,
|
||||
|
||||
Reference in New Issue
Block a user