fix: recover managed npm lock metadata (#107294)

This commit is contained in:
Harjoth Khara
2026-07-14 17:08:18 -07:00
committed by Peter Steinberger
parent 311d9bcc5e
commit 0129eb5916
3 changed files with 408 additions and 76 deletions
+46
View File
@@ -0,0 +1,46 @@
import type { NpmSpecResolution } from "../infra/install-source-utils.js";
import type { ManagedNpmRootInstalledDependency } from "../infra/npm-managed-root.js";
type InstalledNpmResolutionVerification =
| { kind: "ok" }
| { kind: "incomplete"; error: string }
| { kind: "conflict"; error: string };
export function verifyInstalledNpmResolution(params: {
packageName: string;
expected: NpmSpecResolution;
installed: ManagedNpmRootInstalledDependency | null;
}): InstalledNpmResolutionVerification {
if (!params.installed) {
return {
kind: "incomplete",
error: `npm install did not record package-lock metadata for ${params.packageName}`,
};
}
if (params.expected.version && params.installed.version) {
if (params.installed.version !== params.expected.version) {
return {
kind: "conflict",
error: `npm install resolved ${params.packageName} to version ${params.installed.version}, expected ${params.expected.version}`,
};
}
}
if (params.expected.integrity && params.installed.integrity) {
if (params.installed.integrity !== params.expected.integrity) {
return {
kind: "conflict",
error: `npm install resolved ${params.packageName} with integrity ${params.installed.integrity}, expected ${params.expected.integrity}`,
};
}
}
if (
(params.expected.version && !params.installed.version) ||
(params.expected.integrity && !params.installed.integrity)
) {
return {
kind: "incomplete",
error: `npm install recorded incomplete package-lock metadata for ${params.packageName}: ${params.expected.version && !params.installed.version ? "version" : "integrity"} missing`,
};
}
return { kind: "ok" };
}
+296 -3
View File
@@ -120,6 +120,13 @@ function isManagedNpmInstallCommand(argv: unknown): argv is string[] {
return isNpmInstallCommand(argv) && !isNpmPeerPlannerInstallCommand(argv);
}
function managedNpmRootHasDependency(npmRoot: string, packageName: string): boolean {
const manifest = JSON.parse(fs.readFileSync(path.join(npmRoot, "package.json"), "utf8")) as {
dependencies?: Record<string, string>;
};
return packageName in (manifest.dependencies ?? {});
}
function expectNpmInstallIntoRoot(params: {
calls: unknown[][];
npmRoot: string;
@@ -302,6 +309,8 @@ type MockNpmPackage = {
versions?: string[];
installedVersion?: string;
installedIntegrity?: string;
omitInstalledVersion?: boolean;
omitInstalledIntegrity?: boolean;
materializesRootOpenClaw?: boolean;
skipLockfileEntry?: boolean;
packArchivePath?: string;
@@ -324,8 +333,10 @@ function writeNpmRootPackageLock(params: {
continue;
}
lockPackages[`node_modules/${pkg.packageName}`] = {
version: pkg.installedVersion ?? pkg.version,
integrity: pkg.installedIntegrity ?? pkg.integrity ?? "sha512-plugin-test",
...(pkg.omitInstalledVersion ? {} : { version: pkg.installedVersion ?? pkg.version }),
...(pkg.omitInstalledIntegrity
? {}
: { integrity: pkg.installedIntegrity ?? pkg.integrity ?? "sha512-plugin-test" }),
};
if (pkg.materializesRootOpenClaw) {
lockPackages["node_modules/openclaw"] = {
@@ -1205,6 +1216,10 @@ describe("installPluginFromNpmSpec", () => {
it("rejects npm installs when the installed artifact drifts from verified metadata", async () => {
const npmRoot = path.join(suiteTempRootTracker.makeTempDir(), "npm");
const npmProjectRoot = resolvePluginNpmProjectDir({
npmDir: npmRoot,
packageName: "drift-plugin",
});
mockNpmViewAndInstall({
spec: "drift-plugin@latest",
packageName: "drift-plugin",
@@ -1216,6 +1231,21 @@ describe("installPluginFromNpmSpec", () => {
npmRoot,
expectedDependencySpec: "1.0.0",
});
const delegate = runCommandWithTimeoutMock.getMockImplementation();
if (!delegate) {
throw new Error("expected npm mock implementation");
}
let managedInstallAttempts = 0;
runCommandWithTimeoutMock.mockImplementation(async (argv, options) => {
if (
isManagedNpmInstallCommand(argv) &&
options?.cwd === npmProjectRoot &&
managedNpmRootHasDependency(npmProjectRoot, "drift-plugin")
) {
managedInstallAttempts += 1;
}
return await delegate(argv, options);
});
const result = await installPluginFromNpmSpec({
spec: "drift-plugin@latest",
@@ -1230,11 +1260,19 @@ describe("installPluginFromNpmSpec", () => {
}
expect(result.error).toContain("integrity sha512-evil");
expect(result.error).toContain("expected sha512-safe");
expect(managedInstallAttempts).toBe(1);
expect(fs.existsSync(path.join(npmProjectRoot, "_openclaw-quarantined-npm-projects"))).toBe(
false,
);
expect(fs.existsSync(resolveTestPluginPackageDir(npmRoot, "drift-plugin"))).toBe(false);
});
it("rejects npm installs when the installed version drifts from verified metadata", async () => {
const npmRoot = path.join(suiteTempRootTracker.makeTempDir(), "npm");
const npmProjectRoot = resolvePluginNpmProjectDir({
npmDir: npmRoot,
packageName: "version-drift-plugin",
});
mockNpmViewAndInstall({
spec: "version-drift-plugin@latest",
packageName: "version-drift-plugin",
@@ -1244,6 +1282,17 @@ describe("installPluginFromNpmSpec", () => {
npmRoot,
expectedDependencySpec: "1.0.0",
});
const delegate = runCommandWithTimeoutMock.getMockImplementation();
if (!delegate) {
throw new Error("expected npm mock implementation");
}
let managedInstallAttempts = 0;
runCommandWithTimeoutMock.mockImplementation(async (argv, options) => {
if (isManagedNpmInstallCommand(argv) && options?.cwd === npmProjectRoot) {
managedInstallAttempts += 1;
}
return await delegate(argv, options);
});
const result = await installPluginFromNpmSpec({
spec: "version-drift-plugin@latest",
@@ -1257,11 +1306,236 @@ describe("installPluginFromNpmSpec", () => {
}
expect(result.error).toContain("version 1.0.1");
expect(result.error).toContain("expected 1.0.0");
expect(managedInstallAttempts).toBe(1);
expect(fs.existsSync(path.join(npmProjectRoot, "_openclaw-quarantined-npm-projects"))).toBe(
false,
);
expect(fs.existsSync(resolveTestPluginPackageDir(npmRoot, "version-drift-plugin"))).toBe(false);
});
it("rejects npm installs when package-lock omits the installed plugin", async () => {
it("quarantines incomplete integrity metadata and rebuilds the managed project once", async () => {
const npmRoot = path.join(suiteTempRootTracker.makeTempDir(), "npm");
const packageName = "missing-integrity-plugin";
const npmProjectRoot = resolvePluginNpmProjectDir({ npmDir: npmRoot, packageName });
const fixture: MockNpmPackage & { spec: string } = {
spec: `${packageName}@latest`,
packageName,
version: "1.0.0",
pluginId: packageName,
integrity: "sha512-safe",
omitInstalledIntegrity: true,
npmRoot,
expectedDependencySpec: "1.0.0",
};
mockNpmViewAndInstall(fixture);
const delegate = runCommandWithTimeoutMock.getMockImplementation();
if (!delegate) {
throw new Error("expected npm mock implementation");
}
const warnings: string[] = [];
let managedInstallAttempts = 0;
runCommandWithTimeoutMock.mockImplementation(async (argv, options) => {
if (
isManagedNpmInstallCommand(argv) &&
options?.cwd === npmProjectRoot &&
managedNpmRootHasDependency(npmProjectRoot, packageName)
) {
managedInstallAttempts += 1;
if (managedInstallAttempts === 2) {
fixture.omitInstalledIntegrity = false;
}
}
return await delegate(argv, options);
});
const result = await installPluginFromNpmSpec({
spec: fixture.spec,
expectedIntegrity: fixture.integrity,
npmDir: npmRoot,
logger: { info: () => {}, warn: (message) => warnings.push(message) },
});
expect(result.ok).toBe(true);
expect(managedInstallAttempts).toBe(2);
expect(warnings.some((warning) => warning.includes("integrity missing"))).toBe(true);
const installed = JSON.parse(
fs.readFileSync(path.join(npmProjectRoot, "package-lock.json"), "utf8"),
) as { packages?: Record<string, { integrity?: string }> };
expect(installed.packages?.[`node_modules/${packageName}`]?.integrity).toBe("sha512-safe");
expect(
fs.readdirSync(path.join(npmProjectRoot, "_openclaw-quarantined-npm-projects")),
).toHaveLength(1);
});
it.each(["integrity", "version"] as const)(
"fails closed when rebuilt package-lock metadata still omits %s",
async (missingField) => {
const npmRoot = path.join(suiteTempRootTracker.makeTempDir(), "npm");
const packageName = "persistently-incomplete-metadata-plugin";
const npmProjectRoot = resolvePluginNpmProjectDir({ npmDir: npmRoot, packageName });
mockNpmViewAndInstall({
spec: `${packageName}@latest`,
packageName,
version: "1.0.0",
pluginId: packageName,
integrity: "sha512-safe",
omitInstalledIntegrity: missingField === "integrity",
omitInstalledVersion: missingField === "version",
npmRoot,
expectedDependencySpec: "1.0.0",
});
const delegate = runCommandWithTimeoutMock.getMockImplementation();
if (!delegate) {
throw new Error("expected npm mock implementation");
}
let managedInstallAttempts = 0;
runCommandWithTimeoutMock.mockImplementation(async (argv, options) => {
if (
isManagedNpmInstallCommand(argv) &&
options?.cwd === npmProjectRoot &&
managedNpmRootHasDependency(npmProjectRoot, packageName)
) {
managedInstallAttempts += 1;
}
return await delegate(argv, options);
});
const result = await installPluginFromNpmSpec({
spec: `${packageName}@latest`,
expectedIntegrity: "sha512-safe",
npmDir: npmRoot,
logger: { info: () => {}, warn: () => {} },
});
expect(result.ok).toBe(false);
if (result.ok) {
return;
}
expect(managedInstallAttempts).toBe(2);
expect(result.error).toContain(
"metadata remained incomplete after managed npm project recovery",
);
expect(result.error).toContain(`${missingField} missing`);
expect(
fs.readdirSync(path.join(npmProjectRoot, "_openclaw-quarantined-npm-projects")),
).toHaveLength(1);
expect(fs.existsSync(resolveTestPluginPackageDir(npmRoot, packageName))).toBe(false);
},
);
it("does not restore a quarantined tree when post-recovery validation fails", async () => {
const npmRoot = path.join(suiteTempRootTracker.makeTempDir(), "npm");
const packageName = "unsafe-recovered-plugin";
const addedPeerName = "recovery-added-peer";
const npmProjectRoot = resolvePluginNpmProjectDir({ npmDir: npmRoot, packageName });
const stalePackageDir = path.join(npmProjectRoot, "node_modules", "stale-plugin");
fs.mkdirSync(stalePackageDir, { recursive: true });
fs.writeFileSync(path.join(stalePackageDir, "stale.txt"), "poisoned tree", "utf8");
const fixture: MockNpmPackage & { spec: string } = {
spec: `${packageName}@latest`,
packageName,
version: "1.0.0",
pluginId: packageName,
integrity: "sha512-safe",
omitInstalledIntegrity: true,
npmRoot,
expectedDependencySpec: "1.0.0",
hoistedDependency: { name: "plain-crypto-js", version: "1.0.0" },
};
mockNpmViewAndInstallMany([
fixture,
{
packageName: addedPeerName,
version: "2.0.0",
npmRoot,
},
]);
const delegate = runCommandWithTimeoutMock.getMockImplementation();
if (!delegate) {
throw new Error("expected npm mock implementation");
}
let managedInstallAttempts = 0;
runCommandWithTimeoutMock.mockImplementation(async (argv, options) => {
if (
isManagedNpmInstallCommand(argv) &&
options?.cwd === npmProjectRoot &&
managedNpmRootHasDependency(npmProjectRoot, packageName)
) {
managedInstallAttempts += 1;
if (managedInstallAttempts === 2) {
fixture.omitInstalledIntegrity = false;
}
}
return await delegate(argv, options);
});
let mutatedPeerAfterQuarantine = false;
const addPeerAfterQuarantine = () => {
const manifestPath = path.join(npmProjectRoot, "package.json");
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")) as {
dependencies?: Record<string, string>;
openclaw?: { managedPeerDependencies?: string[] };
};
manifest.dependencies ??= {};
manifest.dependencies[addedPeerName] = "2.0.0";
manifest.openclaw ??= {};
manifest.openclaw.managedPeerDependencies = [addedPeerName];
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
mutatedPeerAfterQuarantine = true;
};
const result = await installPluginFromNpmSpec({
spec: fixture.spec,
expectedIntegrity: fixture.integrity,
npmDir: npmRoot,
logger: {
info: () => {},
warn: (message) => {
if (message.includes("quarantined")) {
addPeerAfterQuarantine();
}
},
},
});
expect(result.ok).toBe(false);
if (result.ok) {
return;
}
expect(result.code).toBe(PLUGIN_INSTALL_ERROR_CODE.SECURITY_SCAN_BLOCKED);
expect(managedInstallAttempts).toBe(2);
expect(mutatedPeerAfterQuarantine).toBe(true);
const quarantineParent = path.join(npmProjectRoot, "_openclaw-quarantined-npm-projects");
const quarantines = fs.readdirSync(quarantineParent);
expect(quarantines).toHaveLength(1);
expect(
fs.readFileSync(
path.join(
quarantineParent,
quarantines[0] ?? "",
"node_modules",
"stale-plugin",
"stale.txt",
),
"utf8",
),
).toBe("poisoned tree");
expect(fs.existsSync(stalePackageDir)).toBe(false);
const manifest = JSON.parse(
fs.readFileSync(path.join(npmProjectRoot, "package.json"), "utf8"),
) as {
dependencies?: Record<string, string>;
openclaw?: { managedPeerDependencies?: string[] };
};
expect(manifest.dependencies?.[addedPeerName]).toBeUndefined();
expect(manifest.openclaw?.managedPeerDependencies ?? []).not.toContain(addedPeerName);
});
it("quarantines and retries once when package-lock omits the installed plugin", async () => {
const npmRoot = path.join(suiteTempRootTracker.makeTempDir(), "npm");
const npmProjectRoot = resolvePluginNpmProjectDir({
npmDir: npmRoot,
packageName: "missing-lock-plugin",
});
mockNpmViewAndInstall({
spec: "missing-lock-plugin@latest",
packageName: "missing-lock-plugin",
@@ -1271,6 +1545,21 @@ describe("installPluginFromNpmSpec", () => {
expectedDependencySpec: "1.0.0",
skipLockfileEntry: true,
});
const delegate = runCommandWithTimeoutMock.getMockImplementation();
if (!delegate) {
throw new Error("expected npm mock implementation");
}
let managedInstallAttempts = 0;
runCommandWithTimeoutMock.mockImplementation(async (argv, options) => {
if (
isManagedNpmInstallCommand(argv) &&
options?.cwd === npmProjectRoot &&
managedNpmRootHasDependency(npmProjectRoot, "missing-lock-plugin")
) {
managedInstallAttempts += 1;
}
return await delegate(argv, options);
});
const result = await installPluginFromNpmSpec({
spec: "missing-lock-plugin@latest",
@@ -1285,6 +1574,10 @@ describe("installPluginFromNpmSpec", () => {
expect(result.error).toContain(
"npm install did not record package-lock metadata for missing-lock-plugin",
);
expect(managedInstallAttempts).toBe(2);
expect(
fs.readdirSync(path.join(npmProjectRoot, "_openclaw-quarantined-npm-projects")),
).toHaveLength(1);
expect(fs.existsSync(resolveTestPluginPackageDir(npmRoot, "missing-lock-plugin"))).toBe(false);
});
+66 -73
View File
@@ -51,6 +51,7 @@ import { runCommandWithTimeout } from "../process/exec.js";
import type { InstallPolicySource } from "../security/install-policy.js";
import { createLazyImportLoader } from "../shared/lazy-promise.js";
import { resolveUserPath } from "../utils.js";
import { verifyInstalledNpmResolution } from "./install-npm-resolution.js";
import {
encodePluginInstallDirName,
matchesExpectedPluginId,
@@ -1038,23 +1039,6 @@ async function quarantineManagedNpmProjectRebuildArtifacts(params: {
return { quarantineDir, movedArtifactNames };
}
function resolveInstalledNpmResolutionMismatch(params: {
packageName: string;
expected: NpmSpecResolution;
installed: ManagedNpmRootInstalledDependency | null;
}): string | null {
if (!params.installed) {
return `npm install did not record package-lock metadata for ${params.packageName}`;
}
if (params.expected.version && params.installed.version !== params.expected.version) {
return `npm install resolved ${params.packageName} to version ${params.installed.version ?? "unknown"}, expected ${params.expected.version}`;
}
if (params.expected.integrity && params.installed.integrity !== params.expected.integrity) {
return `npm install resolved ${params.packageName} with integrity ${params.installed.integrity ?? "unknown"}, expected ${params.expected.integrity}`;
}
return null;
}
async function listManagedNpmRootPackageNames(npmRoot: string): Promise<Set<string>> {
const nodeModulesDir = path.join(npmRoot, "node_modules");
let entries: Dirent[];
@@ -1417,6 +1401,15 @@ async function installPluginFromManagedNpmRoot(
let rollbackSnapshot: ManagedNpmPluginInstallRollbackSnapshot;
let preparedDependency: ManagedNpmRootPreparedDependency | undefined;
let rollbackPeerDependencySnapshot:
| Awaited<ReturnType<typeof readManagedNpmRootPeerDependencySnapshot>>
| undefined;
let recovery:
| {
cause: { kind: "npm-corruption" | "incomplete-metadata"; error: string };
quarantine: ManagedNpmProjectQuarantine;
}
| undefined;
try {
rollbackSnapshot = await createManagedNpmPluginInstallRollbackSnapshot({ npmRoot });
} catch (error) {
@@ -1440,11 +1433,8 @@ async function installPluginFromManagedNpmRoot(
logger.info?.(`Repaired stale openclaw peer dependency in ${npmRoot}`);
}
}
let preInstallRootPackageNames = await listManagedNpmRootPackageNames(npmRoot);
const managedOverrides = await readOpenClawManagedNpmRootOverrides();
const rollbackPeerDependencySnapshot = await readManagedNpmRootPeerDependencySnapshot({
npmRoot,
});
rollbackPeerDependencySnapshot ??= await readManagedNpmRootPeerDependencySnapshot({ npmRoot });
const rollbackFailedManagedNpmInstall = async (
failure: Extract<InstallPluginResult, { ok: false }>,
): Promise<Extract<InstallPluginResult, { ok: false }>> => {
@@ -1455,7 +1445,9 @@ async function installPluginFromManagedNpmRoot(
timeoutMs,
logger,
peerDependencySnapshot: rollbackPeerDependencySnapshot,
snapshot: rollbackSnapshot,
// Once the poisoned tree has been quarantined, restoring this snapshot
// would recreate the crash loop that the recovery attempt is repairing.
snapshot: recovery ? undefined : rollbackSnapshot,
});
await rollbackManagedNpmRootPreparedDependency({
packageName: params.packageName,
@@ -1464,23 +1456,22 @@ async function installPluginFromManagedNpmRoot(
});
return failure;
};
const rollbackFailedManagedNpmInstallAfterQuarantine = async (
failure: Extract<InstallPluginResult, { ok: false }>,
): Promise<Extract<InstallPluginResult, { ok: false }>> => {
await rollbackManagedNpmPluginInstall({
npmRoot,
packageName: params.packageName,
targetDir: installRoot,
timeoutMs,
logger,
peerDependencySnapshot: rollbackPeerDependencySnapshot,
});
await rollbackManagedNpmRootPreparedDependency({
packageName: params.packageName,
preparedDependency: prepared,
logger,
});
return failure;
const quarantineForRecovery = async (
cause: NonNullable<typeof recovery>["cause"],
): Promise<Extract<InstallPluginResult, { ok: false }> | null> => {
try {
const quarantine = await quarantineManagedNpmProjectRebuildArtifacts({ npmRoot });
recovery = { cause, quarantine };
} catch (error) {
return await rollbackFailedManagedNpmInstall({
ok: false,
error: `${cause.error}, but OpenClaw could not quarantine ${npmRoot} for rebuild: ${String(error)}`,
});
}
logger.warn?.(
`${cause.error}; quarantined ${formatManagedNpmProjectQuarantineArtifacts(recovery.quarantine.movedArtifactNames)} at ${recovery.quarantine.quarantineDir} and rebuilding once before retrying.`,
);
return null;
};
const syncManagedPeerDependenciesForInstall = async (options?: {
omitUnsupportedManagedOverrides?: boolean;
@@ -1502,13 +1493,18 @@ async function installPluginFromManagedNpmRoot(
};
}
};
let omitUnsupportedManagedOverrides = false;
const preInstallRootPackageNames = await listManagedNpmRootPackageNames(npmRoot);
await upsertManagedNpmRootDependency({
npmRoot,
packageName: params.packageName,
dependencySpec: prepared.dependencySpec,
managedOverrides,
omitUnsupportedManagedOverrides,
});
const initialPeerSync = await syncManagedPeerDependenciesForInstall({
omitUnsupportedManagedOverrides,
});
const initialPeerSync = await syncManagedPeerDependenciesForInstall();
if (!initialPeerSync.ok) {
return await rollbackFailedManagedNpmInstall({ ok: false, error: initialPeerSync.error });
}
@@ -1534,7 +1530,6 @@ async function installPluginFromManagedNpmRoot(
}),
};
let install = await runCommandWithTimeout(npmInstallArgs, npmInstallOptions);
let omitUnsupportedManagedOverrides = false;
if (install.code !== 0 && isNpmAliasOverrideComparatorError(install)) {
logger.warn?.(
"npm rejected managed npm alias overrides; retrying plugin install without alias overrides for this npm version.",
@@ -1558,42 +1553,24 @@ async function installPluginFromManagedNpmRoot(
}
install = await runCommandWithTimeout(npmInstallArgs, npmInstallOptions);
}
if (install.code !== 0 && isManagedNpmProjectCorruptionInstallFailure(install)) {
if (!recovery && install.code !== 0 && isManagedNpmProjectCorruptionInstallFailure(install)) {
const originalError = formatNpmCommandFailureOutput(install);
let quarantine: ManagedNpmProjectQuarantine;
try {
quarantine = await quarantineManagedNpmProjectRebuildArtifacts({ npmRoot });
} catch (error) {
return await rollbackFailedManagedNpmInstall({
ok: false,
error: `npm install failed with a managed npm project corruption signature, but OpenClaw could not quarantine ${npmRoot} for rebuild: ${String(error)}. Original npm error: ${originalError}`,
});
}
logger.warn?.(
`npm reported a managed npm project corruption signature; quarantined ${formatManagedNpmProjectQuarantineArtifacts(quarantine.movedArtifactNames)} at ${quarantine.quarantineDir} and rebuilding once before retrying.`,
);
preInstallRootPackageNames = await listManagedNpmRootPackageNames(npmRoot);
const recoveryPeerSync = await syncManagedPeerDependenciesForInstall({
omitUnsupportedManagedOverrides,
const recoveryFailure = await quarantineForRecovery({
kind: "npm-corruption",
error: `npm install failed with a managed npm project corruption signature. Original npm error: ${originalError}`,
});
if (!recoveryPeerSync.ok) {
return await rollbackFailedManagedNpmInstallAfterQuarantine({
ok: false,
error: `managed npm project recovery failed after quarantining ${formatManagedNpmProjectQuarantineArtifacts(quarantine.movedArtifactNames)} at ${quarantine.quarantineDir}: ${recoveryPeerSync.error}. Original npm error: ${originalError}`,
});
}
install = await runCommandWithTimeout(npmInstallArgs, npmInstallOptions);
if (install.code !== 0) {
return await rollbackFailedManagedNpmInstallAfterQuarantine({
ok: false,
error: `npm install failed after managed npm project recovery (quarantine: ${quarantine.quarantineDir}): ${formatNpmCommandFailureOutput(install)}. Original npm error: ${originalError}`,
});
if (recoveryFailure) {
return recoveryFailure;
}
return await runManagedNpmInstall(prepared);
}
if (install.code !== 0) {
const error = recovery
? `npm install failed after managed npm project recovery (quarantine: ${recovery.quarantine.quarantineDir}): ${formatNpmCommandFailureOutput(install)}. Original ${recovery.cause.kind === "npm-corruption" ? "npm" : "verification"} error: ${recovery.cause.error}`
: `npm install failed: ${formatNpmCommandFailureOutput(install)}`;
return await rollbackFailedManagedNpmInstall({
ok: false,
error: `npm install failed: ${formatNpmCommandFailureOutput(install)}`,
error,
});
}
let settledManagedPeerDependencies = false;
@@ -1760,15 +1737,31 @@ async function installPluginFromManagedNpmRoot(
error: `Failed to verify npm install metadata for ${params.packageName}: ${String(error)}`,
});
}
const resolutionMismatch = resolveInstalledNpmResolutionMismatch({
const resolutionVerification = verifyInstalledNpmResolution({
packageName: params.packageName,
expected: params.npmResolution,
installed: installedDependency,
});
if (resolutionMismatch) {
if (resolutionVerification.kind === "conflict") {
return await rollbackFailedManagedNpmInstall({
ok: false,
error: resolutionMismatch,
error: resolutionVerification.error,
});
}
if (resolutionVerification.kind === "incomplete") {
if (!recovery) {
const recoveryFailure = await quarantineForRecovery({
kind: "incomplete-metadata",
error: resolutionVerification.error,
});
if (recoveryFailure) {
return recoveryFailure;
}
return await runManagedNpmInstall(prepared);
}
return await rollbackFailedManagedNpmInstall({
ok: false,
error: `npm install metadata remained incomplete after managed npm project recovery (quarantine: ${recovery.quarantine.quarantineDir}): ${resolutionVerification.error}`,
});
}