fix(installer): guard WSL installs from Windows Node (#2470)

This commit is contained in:
Davor Racic 2026-06-18 05:19:18 +02:00 committed by GitHub
parent 2417f0048d
commit 5bcc235cdb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 231 additions and 0 deletions

View File

@ -3456,6 +3456,125 @@ async function runTests() {
console.log('');
// ============================================================
// Test Suite 47: WSL shell using Windows Node guard
// ============================================================
console.log(`${colors.yellow}Test Suite 47: WSL Windows Node guard${colors.reset}\n`);
try {
const wslNodeCheck = require('../tools/installer/core/wsl-node-check');
let detection = wslNodeCheck.detectWindowsNodeFromWsl({
platform: 'win32',
env: { WSL_DISTRO_NAME: 'Ubuntu-26.04' },
cwd: String.raw`C:\Windows`,
execPath: String.raw`C:\Program Files\nodejs\node.exe`,
});
assert(detection.isMismatch === true, 'detects Windows Node launched from WSL via WSL_DISTRO_NAME');
detection = wslNodeCheck.detectWindowsNodeFromWsl({
platform: 'win32',
env: { PWD: '/home/devuser/projects/md2pdf' },
cwd: String.raw`\\wsl.localhost\Ubuntu-26.04\home\devuser\projects\md2pdf`,
execPath: String.raw`C:\Program Files\nodejs\node.exe`,
});
assert(detection.isMismatch === true, 'detects Windows Node launched from WSL via Linux PWD / WSL UNC cwd');
detection = wslNodeCheck.detectWindowsNodeFromWsl({
platform: 'win32',
env: {},
cwd: String.raw`\\wsl$\Ubuntu-26.04\home\devuser\projects\md2pdf`,
execPath: String.raw`C:\Program Files\nodejs\node.exe`,
});
assert(detection.isMismatch === true, 'detects Windows Node launched from WSL via legacy WSL UNC cwd');
detection = wslNodeCheck.detectWindowsNodeFromWsl({
platform: 'linux',
env: { WSL_DISTRO_NAME: 'Ubuntu-26.04', PWD: '/home/devuser/projects/md2pdf' },
cwd: '/home/devuser/projects/md2pdf',
execPath: '/usr/bin/node',
});
assert(detection.isMismatch === false, 'allows native Linux Node inside WSL');
detection = wslNodeCheck.detectWindowsNodeFromWsl({
platform: 'win32',
env: { PWD: String.raw`C:\Users\devuser\project` },
cwd: String.raw`C:\Users\devuser\project`,
execPath: String.raw`C:\Program Files\nodejs\node.exe`,
});
assert(detection.isMismatch === false, 'allows normal Windows Node outside WSL');
detection = wslNodeCheck.detectWindowsNodeFromWsl({
platform: 'win32',
env: { PWD: '/c/Users/devuser/project' },
cwd: String.raw`C:\Users\devuser\project`,
execPath: String.raw`C:\Program Files\nodejs\node.exe`,
});
assert(detection.isMismatch === false, 'allows Git Bash Windows-drive PWD outside WSL');
detection = wslNodeCheck.detectWindowsNodeFromWsl({
platform: 'win32',
env: { PWD: '/cygdrive/c/Users/devuser/project' },
cwd: String.raw`C:\Users\devuser\project`,
execPath: String.raw`C:\Program Files\nodejs\node.exe`,
});
assert(detection.isMismatch === false, 'allows Cygwin Windows-drive PWD outside WSL');
const message = wslNodeCheck.formatWindowsNodeFromWslMessage({
isMismatch: true,
reason: 'WSL_DISTRO_NAME is set',
execPath: String.raw`C:\Program Files\nodejs\node.exe`,
});
assert(message.includes('Install Node.js inside WSL'), 'guard message tells user to install Node.js inside WSL');
assert(message.includes(String.raw`C:\Program Files\nodejs\node.exe`), 'guard message includes detected Windows Node path');
const promptsModule = require('../tools/installer/prompts');
const real = {
detectWindowsNodeFromWsl: wslNodeCheck.detectWindowsNodeFromWsl,
log: promptsModule.log,
exit: process.exit,
};
const seen = { errors: [], exit: [] };
wslNodeCheck.detectWindowsNodeFromWsl = () => ({
isMismatch: true,
reason: 'WSL_INTEROP is set',
execPath: String.raw`C:\Program Files\nodejs\node.exe`,
});
promptsModule.log = {
error: async (m) => void seen.errors.push(m),
info: async () => {},
success: async () => {},
warn: async () => {},
message: async () => {},
step: async () => {},
};
process.exit = (code) => {
seen.exit.push(code);
throw new Error('__stub_exit__');
};
try {
let threw = false;
try {
await wslNodeCheck.checkWindowsNodeFromWsl();
} catch (error) {
threw = error.message === '__stub_exit__';
}
assert(threw && seen.exit[0] === 1, 'guard exits with code 1 when Windows Node is launched from WSL');
assert(seen.errors[0].includes('Windows Node.js was launched from a WSL shell'), 'guard logs the mismatch explanation');
} finally {
wslNodeCheck.detectWindowsNodeFromWsl = real.detectWindowsNodeFromWsl;
promptsModule.log = real.log;
process.exit = real.exit;
}
} catch (error) {
console.log(`${colors.red}Test Suite 47 setup failed: ${error.message}${colors.reset}`);
console.log(error.stack);
failed++;
}
console.log('');
// ============================================================
// Summary
// ============================================================

View File

@ -75,6 +75,9 @@ module.exports = {
return;
}
const { checkWindowsNodeFromWsl } = require('../core/wsl-node-check');
await checkWindowsNodeFromWsl();
// Set debug flag as environment variable for all components
if (options.debug) {
process.env.BMAD_DEBUG_MANIFEST = 'true';

View File

@ -0,0 +1,109 @@
const prompts = require('../prompts');
const WSL_UNC_PATTERN = /^\\\\wsl(?:\.localhost|\$)?\\/i;
function normalizePath(value) {
return typeof value === 'string' ? value.replaceAll('/', '\\').toLowerCase() : '';
}
function isLinuxStylePath(value) {
return (
typeof value === 'string' &&
value.startsWith('/') &&
!value.startsWith('//') &&
!/^\/[a-z](?:\/|$)/i.test(value) &&
!/^\/cygdrive\/[a-z](?:\/|$)/i.test(value)
);
}
function isWslUncPath(value) {
return WSL_UNC_PATTERN.test(value || '');
}
/**
* Detect the broken interop case where WSL resolved node/npx to Windows.
* @param {Object} [runtime]
* @param {string} [runtime.platform]
* @param {Object} [runtime.env]
* @param {string} [runtime.cwd]
* @param {string} [runtime.execPath]
* @returns {{isMismatch: boolean, reason: string|null, execPath: string}}
*/
function detectWindowsNodeFromWsl(runtime = {}) {
const platform = runtime.platform || process.platform;
const env = runtime.env || process.env;
const cwd = runtime.cwd || safeCwd();
const execPath = runtime.execPath || process.execPath || '';
if (platform !== 'win32') {
return { isMismatch: false, reason: null, execPath };
}
if (env.WSL_DISTRO_NAME) {
return { isMismatch: true, reason: 'WSL_DISTRO_NAME is set', execPath };
}
if (env.WSL_INTEROP) {
return { isMismatch: true, reason: 'WSL_INTEROP is set', execPath };
}
if (isLinuxStylePath(env.PWD)) {
return { isMismatch: true, reason: 'PWD is a Linux path', execPath };
}
if (isWslUncPath(cwd)) {
return { isMismatch: true, reason: 'current directory is a WSL UNC path', execPath };
}
const normalizedExecPath = normalizePath(execPath);
if (normalizedExecPath.includes('\\wsl$\\') || normalizedExecPath.includes('\\wsl.localhost\\')) {
return { isMismatch: true, reason: 'Node executable path is under a WSL UNC path', execPath };
}
return { isMismatch: false, reason: null, execPath };
}
function safeCwd() {
try {
return process.cwd();
} catch {
return '';
}
}
function formatWindowsNodeFromWslMessage(detection) {
const lines = [
'Windows Node.js was launched from a WSL shell.',
'',
'This usually means Node.js is not installed inside the WSL distro, so WSL resolved `node`/`npx` to Windows.',
'The installer cannot safely continue because Linux paths may be interpreted as Windows paths.',
'',
'Install Node.js inside WSL, then rerun the same command from the WSL terminal.',
];
if (detection.execPath) {
lines.push('', `Detected Node executable: ${detection.execPath}`);
}
if (detection.reason) {
lines.push(`Detection signal: ${detection.reason}`);
}
return lines.join('\n');
}
async function checkWindowsNodeFromWsl() {
const detection = module.exports.detectWindowsNodeFromWsl();
if (!detection.isMismatch) {
return detection;
}
await prompts.log.error(formatWindowsNodeFromWslMessage(detection));
process.exit(1);
}
module.exports = {
checkWindowsNodeFromWsl,
detectWindowsNodeFromWsl,
formatWindowsNodeFromWslMessage,
};