From 9b98301d75caf863528d7200877136e75aed215c Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Tue, 14 Apr 2026 22:14:29 -0400 Subject: [PATCH] fix: handle root cwd when launched from GUI without terminal When apps are launched from macOS Finder, Windows Explorer, or other GUI environments without a terminal, process.cwd() returns the root directory ('/' on Unix, 'C:\' on Windows). This caused issues because: 1. The default preopens mapped '.' to root, which is rarely the intended search directory in GUI apps 2. Reading from root in sandboxed environments can fail or cause permission issues The fix adds isRootPath() helper using path.parse() to detect root directories on both Unix ('/') and Windows ('C:\', 'D:\', etc.). When cwd is root, getDefaultPreopens() returns {} instead of mapping '.' to root. This forces users to provide explicit absolute paths when running in these environments. Changes: - lib/index.mjs: Add isRootPath() and getDefaultPreopens() helpers - test/ripgrep.test.mjs: Add regression test for root cwd scenario - README.md: Document the behavior and workaround for GUI environments --- README.md | 4 +++- lib/index.mjs | 24 ++++++++++++++++++++++-- test/ripgrep.test.mjs | 24 ++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d005a3b..d8936fb 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,9 @@ Options: - `stderr` — writable stream to use instead of `process.stderr`. With `nodeWasi`, only streams with a numeric `fd` property are supported. - `buffer` — when `true`, capture stdout and stderr and return them as strings in the result (`{ code, stdout, stderr }`). Custom `stdout`/`stderr` streams take precedence over buffering for the corresponding channel (default: `false`). - `env` — environment variables passed to the WASI instance (default: `process.env`). -- `preopens` — WASI preopened directories mapping guest paths to host paths (default: `{ ".": process.cwd() }`). Absolute paths passed as args are auto-added as preopens. +- `preopens` — WASI preopened directories mapping guest paths to host paths (default: `{ ".": process.cwd() }`, except when `cwd` is `/` or `\\` in which case it's `{}`). Absolute paths passed as args are auto-added as preopens. + + **Note:** When your app is launched from a GUI (macOS Finder, Windows Explorer, etc.) without a terminal, `process.cwd()` may be `/` (root). In this case, the default preopens excludes `"."` to avoid permission/sandbox issues. You should provide explicit absolute paths in the `preopens` option or as arguments when running in GUI environments. - `returnOnExit` — when `true`, `proc_exit` returns the exit code instead of terminating the process (default: `true`). - `nodeWasi` — use Node's built-in `node:wasi` instead of the bundled WASI shim. Enabled by default on Node.js for best performance; automatically disabled on Bun and Deno where `node:wasi` is not available, falling back to the bundled shim. Can also be forced on via `RIPGREP_NODE_WASI=1`. diff --git a/lib/index.mjs b/lib/index.mjs index bcee43d..984042b 100644 --- a/lib/index.mjs +++ b/lib/index.mjs @@ -1,15 +1,35 @@ import { fileURLToPath } from "node:url"; -import { isAbsolute, resolve } from "node:path"; +import { isAbsolute, resolve, parse } from "node:path"; export const rgPath = fileURLToPath(new URL("./rg.mjs", import.meta.url)); +function isRootPath(cwd) { + // Unix: "/" + // Windows: "C:\\", "D:\\", "C:/", or just "\\" + if (cwd === "/" || cwd === "\\") return true; + const parsed = parse(cwd); + return parsed.root === cwd; +} + +function getDefaultPreopens() { + const cwd = process.cwd(); + // Skip mapping "." to cwd when it's root - this commonly happens + // when apps are launched from GUI (macOS Finder, Windows Explorer, etc.) + // without a terminal. In these cases, "/" or "C:\\" is rarely the intended + // search directory and can cause permission/sandbox issues. + if (isRootPath(cwd)) { + return {}; + } + return { ".": cwd }; +} + export async function ripgrep(args = [], options = {}) { let { stdout, stderr, buffer = false, env = process.env, - preopens = { ".": process.cwd() }, + preopens = getDefaultPreopens(), returnOnExit = true, nodeWasi = getDefaultNodeWasi(), } = options; diff --git a/test/ripgrep.test.mjs b/test/ripgrep.test.mjs index a43de25..652af5c 100644 --- a/test/ripgrep.test.mjs +++ b/test/ripgrep.test.mjs @@ -128,6 +128,30 @@ describe("ripgrep", () => { expect(res.stdout).toMatch(/^ripgrep \d+/); }); + it("works when cwd is root (regression test for GUI launcher issue)", async () => { + // When apps are launched from GUI (Finder, Explorer, etc.), cwd may be "/" + // This should not break ripgrep - it should require explicit paths + const originalCwd = process.cwd(); + const absHello = originalCwd + "/" + HELLO; + + // Only run this test if we can chdir to root + try { + process.chdir("/"); + } catch { + // Skip if we can't chdir to root (permissions) + return; + } + + try { + // With cwd=/, relative paths won't work, but absolute paths should + const res = await ripgrep(["hello", absHello], { buffer: true }); + expect(res.code).toBe(0); + expect(res.stdout).toContain("hello ripgrep world"); + } finally { + process.chdir(originalCwd); + } + }); + it("searches with explicit --color=never", async () => { const res = await ripgrep(["--color=never", "hello", HELLO], { buffer: true,