From b9ede82cc2b83d645cf3ea416eaf682a1751b75c Mon Sep 17 00:00:00 2001 From: jayeshp19 Date: Fri, 3 Apr 2026 13:55:43 +0530 Subject: [PATCH] greptile fix --- src/infra/update-global.test.ts | 83 ++++++++++++++++++++++++++++----- src/infra/update-global.ts | 29 +++++++----- 2 files changed, 90 insertions(+), 22 deletions(-) diff --git a/src/infra/update-global.test.ts b/src/infra/update-global.test.ts index d4e36aefa71..f8e53a0ff93 100644 --- a/src/infra/update-global.test.ts +++ b/src/infra/update-global.test.ts @@ -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 { bundledDistPluginFile } from "../../test/helpers/bundled-plugin-paths.js"; import { BUNDLED_RUNTIME_SIDECAR_PATHS } from "../plugins/runtime-sidecar-paths.js"; import { captureEnv } from "../test-utils/env.js"; @@ -143,10 +143,7 @@ describe("update global helpers", () => { const brewRoot = path.join(brewPrefix, "lib", "node_modules"); const pkgRoot = path.join(brewRoot, "openclaw"); const pathNpmRoot = path.join(base, "nvm", "lib", "node_modules"); - const brewNpm = path.join( - brewBin, - process.platform === "win32" ? "npm.cmd" : "npm", - ); + const brewNpm = path.join(brewBin, process.platform === "win32" ? "npm.cmd" : "npm"); await fs.mkdir(pkgRoot, { recursive: true }); await fs.mkdir(brewBin, { recursive: true }); await fs.writeFile(brewNpm, "", "utf8"); @@ -164,13 +161,9 @@ describe("update global helpers", () => { throw new Error(`unexpected command: ${argv.join(" ")}`); }; - await expect(detectGlobalInstallManagerForRoot(runCommand, pkgRoot, 1000)).resolves.toBe( - "npm", - ); + await expect(detectGlobalInstallManagerForRoot(runCommand, pkgRoot, 1000)).resolves.toBe("npm"); await expect(resolveGlobalRoot("npm", runCommand, 1000, pkgRoot)).resolves.toBe(brewRoot); - await expect(resolveGlobalPackageRoot("npm", runCommand, 1000, pkgRoot)).resolves.toBe( - pkgRoot, - ); + await expect(resolveGlobalPackageRoot("npm", runCommand, 1000, pkgRoot)).resolves.toBe(pkgRoot); expect(globalInstallArgs("npm", "openclaw@latest", pkgRoot)).toEqual([ brewNpm, "i", @@ -192,6 +185,74 @@ describe("update global helpers", () => { ]); }); + it("does not infer npm ownership from path shape alone when the owning npm binary is absent", async () => { + const base = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-update-npm-missing-bin-")); + const brewRoot = path.join(base, "opt", "homebrew", "lib", "node_modules"); + const pkgRoot = path.join(brewRoot, "openclaw"); + const pathNpmRoot = path.join(base, "nvm", "lib", "node_modules"); + await fs.mkdir(pkgRoot, { recursive: true }); + + const runCommand: CommandRunner = async (argv) => { + if (argv[0] === "npm") { + return { stdout: `${pathNpmRoot}\n`, stderr: "", code: 0 }; + } + if (argv[0] === "pnpm") { + return { stdout: "", stderr: "", code: 1 }; + } + throw new Error(`unexpected command: ${argv.join(" ")}`); + }; + + await expect(detectGlobalInstallManagerForRoot(runCommand, pkgRoot, 1000)).resolves.toBeNull(); + expect(globalInstallArgs("npm", "openclaw@latest", pkgRoot)).toEqual([ + "npm", + "i", + "-g", + "openclaw@latest", + "--no-fund", + "--no-audit", + "--loglevel=error", + ]); + }); + + it("prefers npm.cmd for win32-style global npm roots", async () => { + const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); + const base = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-update-win32-npm-prefix-")); + const npmPrefix = path.join(base, "Roaming", "npm"); + const npmRoot = path.join(npmPrefix, "node_modules"); + const pkgRoot = path.join(npmRoot, "openclaw"); + const npmCmd = path.join(npmPrefix, "npm.cmd"); + const pathNpmRoot = path.join(base, "nvm", "node_modules"); + await fs.mkdir(pkgRoot, { recursive: true }); + await fs.writeFile(npmCmd, "", "utf8"); + + const runCommand: CommandRunner = async (argv) => { + if (argv[0] === "npm") { + return { stdout: `${pathNpmRoot}\n`, stderr: "", code: 0 }; + } + if (argv[0] === npmCmd) { + return { stdout: `${npmRoot}\n`, stderr: "", code: 0 }; + } + if (argv[0] === "pnpm") { + return { stdout: "", stderr: "", code: 1 }; + } + throw new Error(`unexpected command: ${argv.join(" ")}`); + }; + + await expect(detectGlobalInstallManagerForRoot(runCommand, pkgRoot, 1000)).resolves.toBe("npm"); + await expect(resolveGlobalRoot("npm", runCommand, 1000, pkgRoot)).resolves.toBe(npmRoot); + expect(globalInstallArgs("npm", "openclaw@latest", pkgRoot)).toEqual([ + npmCmd, + "i", + "-g", + "openclaw@latest", + "--no-fund", + "--no-audit", + "--loglevel=error", + ]); + + platformSpy.mockRestore(); + }); + it("builds install argv and npm fallback argv", () => { expect(globalInstallArgs("npm", "openclaw@latest")).toEqual([ "npm", diff --git a/src/infra/update-global.ts b/src/infra/update-global.ts index 57ca5e388ad..5462ceb7cba 100644 --- a/src/infra/update-global.ts +++ b/src/infra/update-global.ts @@ -191,11 +191,24 @@ function inferNpmPrefixFromPackageRoot(pkgRoot?: string | null): string | null { if (path.basename(nodeModulesDir) !== "node_modules") { return null; } - const libDir = path.dirname(nodeModulesDir); - if (path.basename(libDir) !== "lib") { + const parentDir = path.dirname(nodeModulesDir); + if (path.basename(parentDir) === "lib") { + return path.dirname(parentDir); + } + if (process.platform === "win32" && path.basename(parentDir).toLowerCase() === "npm") { + return parentDir; + } + return null; +} + +function resolvePreferredNpmCommand(pkgRoot?: string | null): string | null { + const prefix = inferNpmPrefixFromPackageRoot(pkgRoot); + if (!prefix) { return null; } - return path.dirname(libDir); + const candidate = + process.platform === "win32" ? path.join(prefix, "npm.cmd") : path.join(prefix, "bin", "npm"); + return fsSync.existsSync(candidate) ? candidate : null; } function resolvePreferredGlobalManagerCommand( @@ -205,13 +218,7 @@ function resolvePreferredGlobalManagerCommand( if (manager !== "npm") { return manager; } - const prefix = inferNpmPrefixFromPackageRoot(pkgRoot); - if (!prefix) { - return manager; - } - const candidate = - process.platform === "win32" ? path.join(prefix, "npm.cmd") : path.join(prefix, "bin", "npm"); - return fsSync.existsSync(candidate) ? candidate : manager; + return resolvePreferredNpmCommand(pkgRoot) ?? manager; } export async function resolveGlobalRoot( @@ -290,7 +297,7 @@ export async function detectGlobalInstallManagerForRoot( } } - if (inferNpmPrefixFromPackageRoot(pkgRoot)) { + if (resolvePreferredNpmCommand(pkgRoot)) { return "npm"; }