fix(installer): guard WSL installs from Windows Node
This commit is contained in:
parent
560a2e3a6f
commit
691f2da6e6
|
|
@ -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
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
Loading…
Reference in New Issue