mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:30:42 +00:00
fix(plugins): fall back to stable official npm versions
This commit is contained in:
@@ -34,6 +34,10 @@ function npmViewArgv(spec: string): string[] {
|
||||
return ["npm", "view", spec, "name", "version", "dist.integrity", "dist.shasum", "--json"];
|
||||
}
|
||||
|
||||
function npmViewVersionsArgv(spec: string): string[] {
|
||||
return ["npm", "view", spec, "versions", "--json"];
|
||||
}
|
||||
|
||||
function expectNpmInstallIntoRoot(params: { calls: unknown[][]; npmRoot: string }) {
|
||||
const installCalls = params.calls.filter(
|
||||
(call) => Array.isArray(call[0]) && call[0][0] === "npm" && call[0][1] === "install",
|
||||
@@ -134,6 +138,7 @@ type MockNpmPackage = {
|
||||
hoistedDependency?: { name: string; version: string };
|
||||
peerDependencies?: Record<string, string>;
|
||||
expectedDependencySpec?: string;
|
||||
versions?: string[];
|
||||
installedVersion?: string;
|
||||
installedIntegrity?: string;
|
||||
skipLockfileEntry?: boolean;
|
||||
@@ -178,6 +183,7 @@ function mockNpmViewAndInstall(params: {
|
||||
hoistedDependency?: { name: string; version: string };
|
||||
peerDependencies?: Record<string, string>;
|
||||
expectedDependencySpec?: string;
|
||||
versions?: string[];
|
||||
installedVersion?: string;
|
||||
installedIntegrity?: string;
|
||||
skipLockfileEntry?: boolean;
|
||||
@@ -204,6 +210,14 @@ function mockNpmViewAndInstallMany(packages: MockNpmPackage[]) {
|
||||
}),
|
||||
);
|
||||
}
|
||||
const versionsPackage = packages.find(
|
||||
(pkg) => JSON.stringify(argv) === JSON.stringify(npmViewVersionsArgv(pkg.packageName)),
|
||||
);
|
||||
if (versionsPackage) {
|
||||
return successfulSpawn(
|
||||
JSON.stringify(versionsPackage.versions ?? [versionsPackage.version]),
|
||||
);
|
||||
}
|
||||
if (argv[0] === "npm" && argv[1] === "install") {
|
||||
const prefixIndex = argv.indexOf("--prefix");
|
||||
const prefixValue = prefixIndex >= 0 ? argv[prefixIndex + 1] : undefined;
|
||||
@@ -882,6 +896,45 @@ describe("installPluginFromNpmSpec", () => {
|
||||
expect(rejected.error).toContain('"@openclaw/voice-call@beta"');
|
||||
}
|
||||
|
||||
runCommandWithTimeoutMock.mockReset();
|
||||
const officialNpmRoot = path.join(suiteTempRootTracker.makeTempDir(), "npm");
|
||||
const warnings: string[] = [];
|
||||
mockNpmViewAndInstallMany([
|
||||
{
|
||||
spec: "@openclaw/voice-call",
|
||||
packageName: "@openclaw/voice-call",
|
||||
version: "0.0.2-beta.1",
|
||||
npmRoot: officialNpmRoot,
|
||||
versions: ["0.0.1", "0.0.2-beta.1"],
|
||||
},
|
||||
{
|
||||
spec: "@openclaw/voice-call@0.0.1",
|
||||
packageName: "@openclaw/voice-call",
|
||||
version: "0.0.1",
|
||||
pluginId: "voice-call",
|
||||
npmRoot: officialNpmRoot,
|
||||
expectedDependencySpec: "0.0.1",
|
||||
},
|
||||
]);
|
||||
|
||||
const officialFallback = await installPluginFromNpmSpec({
|
||||
spec: "@openclaw/voice-call",
|
||||
npmDir: officialNpmRoot,
|
||||
expectedPluginId: "voice-call",
|
||||
trustedSourceLinkedOfficialInstall: true,
|
||||
logger: {
|
||||
info: () => {},
|
||||
warn: (msg: string) => warnings.push(msg),
|
||||
},
|
||||
});
|
||||
expect(officialFallback.ok).toBe(true);
|
||||
if (!officialFallback.ok) {
|
||||
return;
|
||||
}
|
||||
expect(officialFallback.npmResolution?.version).toBe("0.0.1");
|
||||
expect(officialFallback.npmResolution?.resolvedSpec).toBe("@openclaw/voice-call@0.0.1");
|
||||
expect(warnings.join("\n")).toContain("falling back to stable @openclaw/voice-call@0.0.1");
|
||||
|
||||
runCommandWithTimeoutMock.mockReset();
|
||||
const npmRoot = path.join(suiteTempRootTracker.makeTempDir(), "npm");
|
||||
mockNpmViewAndInstall({
|
||||
|
||||
@@ -16,8 +16,11 @@ import {
|
||||
} from "../infra/npm-managed-root.js";
|
||||
import {
|
||||
formatPrereleaseResolutionError,
|
||||
isExactSemverVersion,
|
||||
isPrereleaseSemverVersion,
|
||||
isPrereleaseResolutionAllowed,
|
||||
parseRegistryNpmSpec,
|
||||
type ParsedRegistryNpmSpec,
|
||||
} from "../infra/npm-registry-spec.js";
|
||||
import {
|
||||
createSafeNpmInstallArgs,
|
||||
@@ -157,6 +160,68 @@ function isNpmPackageNotFoundMessage(error: string): boolean {
|
||||
return /E404|404 not found|not in this registry/i.test(normalized);
|
||||
}
|
||||
|
||||
function compareStableSemver(a: string, b: string): number {
|
||||
const parse = (value: string): [number, number, number] => {
|
||||
const match = /^v?(\d+)\.(\d+)\.(\d+)/.exec(value.trim());
|
||||
return [Number(match?.[1] ?? 0), Number(match?.[2] ?? 0), Number(match?.[3] ?? 0)];
|
||||
};
|
||||
const left = parse(a);
|
||||
const right = parse(b);
|
||||
return left[0] - right[0] || left[1] - right[1] || left[2] - right[2];
|
||||
}
|
||||
|
||||
async function resolveTrustedOfficialStableNpmResolution(params: {
|
||||
spec: ParsedRegistryNpmSpec;
|
||||
resolvedPrereleaseVersion: string;
|
||||
timeoutMs: number;
|
||||
logger: PluginInstallLogger;
|
||||
}): Promise<NpmSpecResolution | null> {
|
||||
if (!params.spec.name.startsWith("@openclaw/")) {
|
||||
return null;
|
||||
}
|
||||
const versions = await runCommandWithTimeout(
|
||||
["npm", "view", params.spec.name, "versions", "--json"],
|
||||
{
|
||||
timeoutMs: Math.max(params.timeoutMs, 60_000),
|
||||
env: {
|
||||
COREPACK_ENABLE_DOWNLOAD_PROMPT: "0",
|
||||
NPM_CONFIG_IGNORE_SCRIPTS: "true",
|
||||
},
|
||||
},
|
||||
);
|
||||
if (versions.code !== 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(versions.stdout.trim());
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
const stableVersion = (Array.isArray(parsed) ? parsed : [parsed])
|
||||
.filter((value): value is string => typeof value === "string")
|
||||
.filter((value) => isExactSemverVersion(value) && !isPrereleaseSemverVersion(value))
|
||||
.sort(compareStableSemver)
|
||||
.at(-1);
|
||||
if (!stableVersion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const stableSpec = `${params.spec.name}@${stableVersion}`;
|
||||
const metadataResult = await resolveNpmSpecMetadata({
|
||||
spec: stableSpec,
|
||||
timeoutMs: params.timeoutMs,
|
||||
});
|
||||
if (!metadataResult.ok) {
|
||||
return null;
|
||||
}
|
||||
params.logger.warn?.(
|
||||
`Resolved ${params.spec.raw} to prerelease version ${params.resolvedPrereleaseVersion}; falling back to stable ${stableSpec} for this trusted official OpenClaw install.`,
|
||||
);
|
||||
return metadataResult.metadata;
|
||||
}
|
||||
|
||||
function buildFileInstallResult(pluginId: string, targetFile: string): InstallPluginResult {
|
||||
return {
|
||||
ok: true,
|
||||
@@ -1180,13 +1245,27 @@ export async function installPluginFromNpmSpec(
|
||||
resolvedVersion: npmResolution.version,
|
||||
})
|
||||
) {
|
||||
return {
|
||||
ok: false,
|
||||
error: formatPrereleaseResolutionError({
|
||||
spec: parsedSpec,
|
||||
resolvedVersion: npmResolution.version,
|
||||
}),
|
||||
};
|
||||
const stableResolution = params.trustedSourceLinkedOfficialInstall
|
||||
? await resolveTrustedOfficialStableNpmResolution({
|
||||
spec: parsedSpec,
|
||||
resolvedPrereleaseVersion: npmResolution.version,
|
||||
timeoutMs,
|
||||
logger,
|
||||
})
|
||||
: null;
|
||||
if (stableResolution) {
|
||||
Object.assign(npmResolution, stableResolution, {
|
||||
resolvedAt: npmResolution.resolvedAt,
|
||||
});
|
||||
} else {
|
||||
return {
|
||||
ok: false,
|
||||
error: formatPrereleaseResolutionError({
|
||||
spec: parsedSpec,
|
||||
resolvedVersion: npmResolution.version,
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
const driftResult = await resolveNpmIntegrityDriftWithDefaultMessage({
|
||||
spec,
|
||||
|
||||
Reference in New Issue
Block a user