mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-06 15:18:58 +00:00
fix(plugins): clear hidden npm lock during peer repair
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
repairManagedNpmRootOpenClawPeer,
|
||||
removeManagedNpmRootDependency,
|
||||
@@ -12,6 +12,15 @@ import {
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
const successfulSpawn = {
|
||||
code: 0,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
signal: null,
|
||||
killed: false,
|
||||
termination: "exit" as const,
|
||||
};
|
||||
|
||||
async function makeTempRoot(): Promise<string> {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-npm-managed-root-"));
|
||||
tempDirs.push(dir);
|
||||
@@ -219,8 +228,40 @@ describe("managed npm root", () => {
|
||||
path.join(npmRoot, "node_modules", "openclaw", "package.json"),
|
||||
`${JSON.stringify({ name: "openclaw", version: "2026.5.4" })}\n`,
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(npmRoot, "node_modules", ".package-lock.json"),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
lockfileVersion: 3,
|
||||
packages: {
|
||||
"node_modules/openclaw": {
|
||||
version: "2026.5.4",
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
|
||||
await expect(repairManagedNpmRootOpenClawPeer({ npmRoot })).resolves.toBe(true);
|
||||
const runCommand = vi.fn().mockResolvedValue(successfulSpawn);
|
||||
await expect(repairManagedNpmRootOpenClawPeer({ npmRoot, runCommand })).resolves.toBe(true);
|
||||
expect(runCommand).toHaveBeenCalledWith(
|
||||
[
|
||||
"npm",
|
||||
"uninstall",
|
||||
"--loglevel=error",
|
||||
"--ignore-scripts",
|
||||
"--no-audit",
|
||||
"--no-fund",
|
||||
"--prefix",
|
||||
".",
|
||||
"openclaw",
|
||||
],
|
||||
expect.objectContaining({
|
||||
cwd: npmRoot,
|
||||
}),
|
||||
);
|
||||
|
||||
const manifest = JSON.parse(await fs.readFile(path.join(npmRoot, "package.json"), "utf8")) as {
|
||||
dependencies?: Record<string, string>;
|
||||
@@ -243,5 +284,10 @@ describe("managed npm root", () => {
|
||||
await expect(fs.lstat(path.join(npmRoot, "node_modules", "openclaw"))).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
await expect(
|
||||
fs.lstat(path.join(npmRoot, "node_modules", ".package-lock.json")),
|
||||
).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { runCommandWithTimeout } from "../process/exec.js";
|
||||
import type { NpmSpecResolution } from "./install-source-utils.js";
|
||||
import type { ParsedRegistryNpmSpec } from "./npm-registry-spec.js";
|
||||
import { createSafeNpmInstallEnv } from "./safe-package-install.js";
|
||||
|
||||
type ManagedNpmRootManifest = {
|
||||
private?: boolean;
|
||||
@@ -21,6 +23,12 @@ type ManagedNpmRootLockfile = {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
type ManagedNpmRootLogger = {
|
||||
warn?: (message: string) => void;
|
||||
};
|
||||
|
||||
type ManagedNpmRootRunCommand = typeof runCommandWithTimeout;
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
@@ -83,10 +91,105 @@ export async function upsertManagedNpmRootDependency(params: {
|
||||
|
||||
export async function repairManagedNpmRootOpenClawPeer(params: {
|
||||
npmRoot: string;
|
||||
timeoutMs?: number;
|
||||
logger?: ManagedNpmRootLogger;
|
||||
runCommand?: ManagedNpmRootRunCommand;
|
||||
}): Promise<boolean> {
|
||||
let changed = false;
|
||||
|
||||
await fs.mkdir(params.npmRoot, { recursive: true });
|
||||
|
||||
const manifestPath = path.join(params.npmRoot, "package.json");
|
||||
const manifest = await readManagedNpmRootManifest(manifestPath);
|
||||
const dependencies = readDependencyRecord(manifest.dependencies);
|
||||
const hasManifestDependency = "openclaw" in dependencies;
|
||||
const hasLockDependency = await managedNpmRootLockfileHasOpenClawPeer(params.npmRoot);
|
||||
const hasPackageDir = await pathExists(path.join(params.npmRoot, "node_modules", "openclaw"));
|
||||
if (!hasManifestDependency && !hasLockDependency && !hasPackageDir) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const command = params.runCommand ?? runCommandWithTimeout;
|
||||
const npmArgs = hasManifestDependency
|
||||
? [
|
||||
"npm",
|
||||
"uninstall",
|
||||
"--loglevel=error",
|
||||
"--ignore-scripts",
|
||||
"--no-audit",
|
||||
"--no-fund",
|
||||
"--prefix",
|
||||
".",
|
||||
"openclaw",
|
||||
]
|
||||
: [
|
||||
"npm",
|
||||
"prune",
|
||||
"--loglevel=error",
|
||||
"--ignore-scripts",
|
||||
"--no-audit",
|
||||
"--no-fund",
|
||||
"--prefix",
|
||||
".",
|
||||
];
|
||||
try {
|
||||
const result = await command(npmArgs, {
|
||||
cwd: params.npmRoot,
|
||||
timeoutMs: Math.max(params.timeoutMs ?? 300_000, 300_000),
|
||||
env: createSafeNpmInstallEnv(process.env, { packageLock: true, quiet: true }),
|
||||
});
|
||||
if (result.code !== 0) {
|
||||
params.logger?.warn?.(
|
||||
`npm ${hasManifestDependency ? "uninstall openclaw" : "prune"} failed while repairing managed npm root; falling back to direct cleanup: ${result.stderr.trim() || result.stdout.trim()}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
params.logger?.warn?.(
|
||||
`npm ${hasManifestDependency ? "uninstall openclaw" : "prune"} failed while repairing managed npm root; falling back to direct cleanup: ${String(error)}`,
|
||||
);
|
||||
}
|
||||
|
||||
await scrubManagedNpmRootOpenClawPeer({ npmRoot: params.npmRoot });
|
||||
return true;
|
||||
}
|
||||
|
||||
async function managedNpmRootLockfileHasOpenClawPeer(npmRoot: string): Promise<boolean> {
|
||||
const lockPath = path.join(npmRoot, "package-lock.json");
|
||||
try {
|
||||
const parsed = JSON.parse(await fs.readFile(lockPath, "utf8")) as ManagedNpmRootLockfile;
|
||||
if (isRecord(parsed.packages)) {
|
||||
const rootPackage = parsed.packages[""];
|
||||
if (
|
||||
isRecord(rootPackage) &&
|
||||
isRecord(rootPackage.dependencies) &&
|
||||
"openclaw" in rootPackage.dependencies
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if ("node_modules/openclaw" in parsed.packages) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return isRecord(parsed.dependencies) && "openclaw" in parsed.dependencies;
|
||||
} catch (err) {
|
||||
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
||||
return false;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function pathExists(filePath: string): Promise<boolean> {
|
||||
return await fs
|
||||
.lstat(filePath)
|
||||
.then(() => true)
|
||||
.catch((err: NodeJS.ErrnoException) => {
|
||||
if (err.code === "ENOENT") {
|
||||
return false;
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
async function scrubManagedNpmRootOpenClawPeer(params: { npmRoot: string }): Promise<void> {
|
||||
const manifestPath = path.join(params.npmRoot, "package.json");
|
||||
const manifest = await readManagedNpmRootManifest(manifestPath);
|
||||
const dependencies = readDependencyRecord(manifest.dependencies);
|
||||
@@ -97,7 +200,6 @@ export async function repairManagedNpmRootOpenClawPeer(params: {
|
||||
`${JSON.stringify({ ...manifest, private: true, dependencies: nextDependencies }, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
const lockPath = path.join(params.npmRoot, "package-lock.json");
|
||||
@@ -127,7 +229,6 @@ export async function repairManagedNpmRootOpenClawPeer(params: {
|
||||
}
|
||||
if (lockChanged) {
|
||||
await fs.writeFile(lockPath, `${JSON.stringify(parsed, null, 2)}\n`, "utf8");
|
||||
changed = true;
|
||||
}
|
||||
} catch (err) {
|
||||
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
|
||||
@@ -136,21 +237,12 @@ export async function repairManagedNpmRootOpenClawPeer(params: {
|
||||
}
|
||||
|
||||
const openclawPackageDir = path.join(params.npmRoot, "node_modules", "openclaw");
|
||||
const openclawPackageDirExists = await fs
|
||||
.lstat(openclawPackageDir)
|
||||
.then(() => true)
|
||||
.catch((err: NodeJS.ErrnoException) => {
|
||||
if (err.code === "ENOENT") {
|
||||
return false;
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
if (openclawPackageDirExists) {
|
||||
if (await pathExists(openclawPackageDir)) {
|
||||
await fs.rm(openclawPackageDir, { recursive: true, force: true });
|
||||
changed = true;
|
||||
}
|
||||
|
||||
return changed;
|
||||
await fs.rm(path.join(params.npmRoot, "node_modules", ".package-lock.json"), {
|
||||
force: true,
|
||||
});
|
||||
}
|
||||
|
||||
export async function readManagedNpmRootInstalledDependency(params: {
|
||||
|
||||
@@ -257,6 +257,19 @@ function mockNpmViewAndInstallMany(packages: MockNpmPackage[]) {
|
||||
}
|
||||
if (argv[0] === "npm" && argv[1] === "uninstall") {
|
||||
const packageName = argv.at(-1);
|
||||
if (packageName === "openclaw") {
|
||||
const prefixIndex = argv.indexOf("--prefix");
|
||||
const prefixValue = prefixIndex >= 0 ? argv[prefixIndex + 1] : undefined;
|
||||
const npmRoot = prefixValue === "." ? options?.cwd : prefixValue;
|
||||
if (!npmRoot) {
|
||||
throw new Error(`unexpected npm uninstall command: ${argv.join(" ")}`);
|
||||
}
|
||||
fs.rmSync(path.join(npmRoot, "node_modules", "openclaw"), {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
return successfulSpawn();
|
||||
}
|
||||
const pkg = packageName ? packagesByName.get(packageName) : undefined;
|
||||
if (!pkg) {
|
||||
throw new Error(`unexpected npm uninstall package: ${packageName ?? ""}`);
|
||||
|
||||
@@ -1337,7 +1337,11 @@ export async function installPluginFromNpmSpec(
|
||||
|
||||
logger.info?.(`Installing ${spec} into ${npmRoot}…`);
|
||||
if (parsedSpec.name !== "openclaw") {
|
||||
const repairedOpenClawPeer = await repairManagedNpmRootOpenClawPeer({ npmRoot });
|
||||
const repairedOpenClawPeer = await repairManagedNpmRootOpenClawPeer({
|
||||
npmRoot,
|
||||
timeoutMs,
|
||||
logger,
|
||||
});
|
||||
if (repairedOpenClawPeer) {
|
||||
logger.info?.(`Repaired stale openclaw peer dependency in ${npmRoot}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user