























@@ -61,6 +61,7 @@ function writeInstalledNpmPlugin(params: {
6161indexJs?: string;
6262dependency?: { name: string; version: string };
6363hoistedDependency?: { name: string; version: string };
64+peerDependencies?: Record<string, string>;
6465}) {
6566const pluginDir = path.join(params.npmRoot, "node_modules", params.packageName);
6667fs.mkdirSync(path.join(pluginDir, "dist"), { recursive: true });
@@ -73,6 +74,7 @@ function writeInstalledNpmPlugin(params: {
7374 ...(params.dependency
7475 ? { dependencies: { [params.dependency.name]: params.dependency.version } }
7576 : {}),
77+ ...(params.peerDependencies ? { peerDependencies: params.peerDependencies } : {}),
7678}),
7779"utf-8",
7880);
@@ -128,26 +130,60 @@ function mockNpmViewAndInstall(params: {
128130indexJs?: string;
129131dependency?: { name: string; version: string };
130132hoistedDependency?: { name: string; version: string };
133+peerDependencies?: Record<string, string>;
131134}) {
135+mockNpmViewAndInstallMany([params]);
136+}
137+138+function mockNpmViewAndInstallMany(
139+packages: Array<{
140+spec: string;
141+packageName: string;
142+version: string;
143+npmRoot: string;
144+pluginId?: string;
145+integrity?: string;
146+shasum?: string;
147+indexJs?: string;
148+dependency?: { name: string; version: string };
149+hoistedDependency?: { name: string; version: string };
150+peerDependencies?: Record<string, string>;
151+}>,
152+) {
153+const packagesBySpec = new Map(packages.map((pkg) => [pkg.spec, pkg]));
154+const packagesByName = new Map(packages.map((pkg) => [pkg.packageName, pkg]));
132155runCommandWithTimeoutMock.mockImplementation(async (argv: string[]) => {
133-if (JSON.stringify(argv) === JSON.stringify(npmViewArgv(params.spec))) {
156+const viewPackage = packages.find(
157+(pkg) => JSON.stringify(argv) === JSON.stringify(npmViewArgv(pkg.spec)),
158+);
159+if (viewPackage) {
134160return successfulSpawn(
135161JSON.stringify({
136-name: params.packageName,
137-version: params.version,
162+name: viewPackage.packageName,
163+version: viewPackage.version,
138164dist: {
139-integrity: params.integrity ?? "sha512-plugin-test",
140-shasum: params.shasum ?? "pluginshasum",
165+integrity: viewPackage.integrity ?? "sha512-plugin-test",
166+shasum: viewPackage.shasum ?? "pluginshasum",
141167},
142168}),
143169);
144170}
145171if (argv[0] === "npm" && argv[1] === "install") {
146-writeInstalledNpmPlugin(params);
172+const spec = argv.at(-1);
173+const pkg = spec ? packagesBySpec.get(spec) : undefined;
174+if (!pkg) {
175+throw new Error(`unexpected npm install spec: ${spec ?? ""}`);
176+}
177+writeInstalledNpmPlugin(pkg);
147178return successfulSpawn();
148179}
149180if (argv[0] === "npm" && argv[1] === "uninstall") {
150-fs.rmSync(path.join(params.npmRoot, "node_modules", params.packageName), {
181+const packageName = argv.at(-1);
182+const pkg = packageName ? packagesByName.get(packageName) : undefined;
183+if (!pkg) {
184+throw new Error(`unexpected npm uninstall package: ${packageName ?? ""}`);
185+}
186+fs.rmSync(path.join(pkg.npmRoot, "node_modules", pkg.packageName), {
151187recursive: true,
152188force: true,
153189});
@@ -230,6 +266,55 @@ describe("installPluginFromNpmSpec", () => {
230266}
231267});
232268269+it.runIf(process.platform !== "win32")(
270+"does not let managed openclaw peer links poison later npm installs",
271+async () => {
272+const stateDir = suiteTempRootTracker.makeTempDir();
273+const npmRoot = path.join(stateDir, "npm");
274+275+mockNpmViewAndInstallMany([
276+{
277+spec: "peer-plugin@1.0.0",
278+packageName: "peer-plugin",
279+version: "1.0.0",
280+pluginId: "peer-plugin",
281+ npmRoot,
282+peerDependencies: { openclaw: "^2026.0.0" },
283+},
284+{
285+spec: "next-plugin@1.0.0",
286+packageName: "next-plugin",
287+version: "1.0.0",
288+pluginId: "next-plugin",
289+ npmRoot,
290+},
291+]);
292+293+const first = await installPluginFromNpmSpec({
294+spec: "peer-plugin@1.0.0",
295+npmDir: npmRoot,
296+logger: { info: () => {}, warn: () => {} },
297+});
298+expect(first.ok).toBe(true);
299+expect(
300+fs
301+.lstatSync(path.join(npmRoot, "node_modules", "peer-plugin", "node_modules", "openclaw"))
302+.isSymbolicLink(),
303+).toBe(true);
304+305+const second = await installPluginFromNpmSpec({
306+spec: "next-plugin@1.0.0",
307+npmDir: npmRoot,
308+logger: { info: () => {}, warn: () => {} },
309+});
310+311+expect(second.ok).toBe(true);
312+if (!second.ok) {
313+expect(second.error).not.toContain("peer-plugin/node_modules/openclaw");
314+}
315+},
316+);
317+233318it("allows npm-spec installs with dangerous code patterns when forced unsafe install is set", async () => {
234319const npmRoot = path.join(suiteTempRootTracker.makeTempDir(), "npm");
235320const warnings: string[] = [];
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。