diff --git a/scripts/ripgrep-wrapper.js b/scripts/ripgrep-wrapper.js index 7974636b..62b33b99 100644 --- a/scripts/ripgrep-wrapper.js +++ b/scripts/ripgrep-wrapper.js @@ -1,13 +1,24 @@ -// Runtime platform detection wrapper for @vscode/ripgrep -// This replaces the original index.js to support cross-platform MCPB bundles +// Runtime platform detection wrapper for @vscode/ripgrep. +// This replaces the package's index.js in MCPB bundles so a single bundle can +// resolve the correct ripgrep binary at runtime across platforms. +// +// IMPORTANT: @vscode/ripgrep 1.18.0+ ships package.json with "type": "module", +// so this wrapper MUST be ESM. The previous CommonJS version (require / +// module.exports) threw "require is not defined in ES module scope" the moment +// the dependency went ESM. That error was swallowed by the resolver's try/catch +// and surfaced as a misleading "ripgrep binary not found", silently breaking +// search on Windows even though the bundled binaries were present. -const os = require('os'); -const path = require('path'); -const fs = require('fs'); +import os from 'os'; +import path from 'path'; +import fs from 'fs'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); function getTarget() { const arch = process.env.npm_config_arch || os.arch(); - + switch (os.platform()) { case 'darwin': return arch === 'arm64' ? 'aarch64-apple-darwin' : 'x86_64-apple-darwin'; @@ -32,34 +43,25 @@ const target = getTarget(); const isWindows = os.platform() === 'win32'; const binaryName = isWindows ? `rg-${target}.exe` : `rg-${target}`; // __dirname is lib/, so go up one level to reach bin/ -const rgPath = path.join(__dirname, '..', 'bin', binaryName); +let resolvedRgPath = path.join(__dirname, '..', 'bin', binaryName); -// Verify binary exists and ensure executable permissions -if (!fs.existsSync(rgPath)) { - // Try fallback to original rg location +if (!fs.existsSync(resolvedRgPath)) { + // Fallback to a plain rg binary if the platform-specific one is missing const fallbackPath = path.join(__dirname, '..', 'bin', isWindows ? 'rg.exe' : 'rg'); - if (fs.existsSync(fallbackPath)) { - // Ensure executable permissions on Unix systems - if (!isWindows) { - try { - fs.chmodSync(fallbackPath, 0o755); - } catch (err) { - // Ignore permission errors - might not have write access - } - } - module.exports.rgPath = fallbackPath; - } else { - throw new Error(`ripgrep binary not found for platform ${target}: ${rgPath}`); + if (!fs.existsSync(fallbackPath)) { + throw new Error(`ripgrep binary not found for platform ${target}: ${resolvedRgPath}`); } -} else { - // Ensure executable permissions on Unix systems - // This fixes issues when extracting from zip archives that don't preserve permissions - if (!isWindows) { - try { - fs.chmodSync(rgPath, 0o755); - } catch (err) { - // Ignore permission errors - might not have write access - } + resolvedRgPath = fallbackPath; +} + +// Ensure executable permissions on Unix systems. +// Fixes issues when extracting from zip archives that don't preserve permissions. +if (!isWindows) { + try { + fs.chmodSync(resolvedRgPath, 0o755); + } catch (err) { + // Ignore permission errors - might not have write access } - module.exports.rgPath = rgPath; } + +export const rgPath = resolvedRgPath; diff --git a/src/terminal-manager.ts b/src/terminal-manager.ts index ee991709..00ffb6f2 100644 --- a/src/terminal-manager.ts +++ b/src/terminal-manager.ts @@ -6,6 +6,35 @@ import { configManager } from './config-manager.js'; import {capture} from "./utils/capture.js"; import { analyzeProcessState } from './utils/process-detection.js'; +/** + * Standard Windows PATHEXT value, used to repair a corrupted PATHEXT before + * spawning child shells. + * + * On some Windows Claude Desktop / DXT launches the server process inherits a + * broken PATHEXT (observed as ".CPL" only). Because we build the child env from + * { ...process.env }, that broken value would propagate into every spawned + * shell, stripping ".EXE" and breaking resolution of git / node / python / rg / + * etc. (and even full-path .exe invocations under PowerShell). See issue #481. + */ +const STANDARD_PATHEXT = '.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC'; + +/** + * Return a healthy PATHEXT for spawned Windows shells. + * - Unset -> use the standard list. + * - Missing ".EXE" -> corrupted; merge the standard list with whatever was + * present (preserves any extra extensions, order-stable). + * - Otherwise -> leave the inherited value untouched. + */ +function getRepairedPathExt(): string { + const current = process.env.PATHEXT; + if (!current) return STANDARD_PATHEXT; + const exts = current.split(';').map(e => e.trim().toUpperCase()).filter(Boolean); + if (!exts.includes('.EXE')) { + return [...new Set([...STANDARD_PATHEXT.split(';'), ...exts])].join(';'); + } + return current; +} + interface CompletedSession { pid: number; outputLines: string[]; // Line-based buffer (consistent with active sessions) @@ -185,6 +214,14 @@ export class TerminalManager { }; } + // Repair PATHEXT on Windows before spawning. On some Windows DXT launches + // the server process inherits a corrupted PATHEXT (e.g. ".CPL"), which we + // would otherwise propagate via { ...process.env } and break command + // resolution (git, node, python, rg, ...) in the spawned shell. See #481. + if (process.platform === 'win32' && spawnOptions.env) { + spawnOptions.env.PATHEXT = getRepairedPathExt(); + } + // Spawn the process with appropriate arguments const childProcess = spawn(spawnConfig.executable, spawnConfig.args, spawnOptions); let output = '';