Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 34 additions & 32 deletions scripts/ripgrep-wrapper.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;
37 changes: 37 additions & 0 deletions src/terminal-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 = '';
Expand Down
Loading