fix(windows): run bundled openclaw CLI/TUI via node.exe to restore terminal input (#571)

This commit is contained in:
Felix
2026-03-19 13:25:00 +08:00
committed by GitHub
Unverified
parent 2253fed5a1
commit 78a9eb755b
11 changed files with 285 additions and 60 deletions

View File

@@ -172,27 +172,54 @@ function patchBrokenModules(nodeModulesDir) {
}
}
// https-proxy-agent@8.x only defines exports.import (ESM) with no CJS
// fallback. The openclaw Gateway loads it via require(), which triggers
// ERR_PACKAGE_PATH_NOT_EXPORTED. Patch exports to add CJS conditions.
// https-proxy-agent: add a CJS `require` condition only when we can point to
// a real CommonJS entry. Mapping `require` to an ESM file can cause
// ERR_REQUIRE_CYCLE_MODULE in Node.js CLI/TUI flows.
const hpaPkgPath = join(nodeModulesDir, 'https-proxy-agent', 'package.json');
if (existsSync(hpaPkgPath)) {
try {
const { existsSync: fsExistsSync } = require('fs');
const raw = readFileSync(hpaPkgPath, 'utf8');
const pkg = JSON.parse(raw);
const exp = pkg.exports;
// Only patch if exports exists and lacks a CJS 'require' condition
if (exp && exp.import && !exp.require && !exp['.']) {
const hasRequireCondition = Boolean(
(exp && typeof exp === 'object' && exp.require) ||
(exp && typeof exp === 'object' && exp['.'] && exp['.'].require)
);
const pkgDir = dirname(hpaPkgPath);
const mainEntry = typeof pkg.main === 'string' ? pkg.main : null;
const dotImport = exp && typeof exp === 'object' && exp['.'] && typeof exp['.'].import === 'string'
? exp['.'].import
: null;
const rootImport = exp && typeof exp === 'object' && typeof exp.import === 'string'
? exp.import
: null;
const importEntry = dotImport || rootImport;
const cjsCandidates = [
mainEntry,
importEntry && importEntry.endsWith('.js') ? importEntry.replace(/\.js$/, '.cjs') : null,
'./dist/index.cjs',
].filter(Boolean);
const requireTarget = cjsCandidates.find((candidate) =>
fsExistsSync(join(pkgDir, candidate)),
);
// Only patch if exports exists, lacks a CJS `require` condition, and we
// have a verified CJS target file.
if (exp && !hasRequireCondition && requireTarget) {
pkg.exports = {
'.': {
import: exp.import,
require: exp.import, // ESM dist works for CJS too via Node.js interop
default: typeof exp.import === 'string' ? exp.import : exp.import.default,
import: importEntry || requireTarget,
require: requireTarget,
default: importEntry || requireTarget,
},
};
writeFileSync(hpaPkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
count++;
console.log('[after-pack] 🩹 Patched https-proxy-agent exports for CJS compatibility');
console.log(`[after-pack] 🩹 Patched https-proxy-agent exports for CJS compatibility (require=${requireTarget})`);
}
} catch (err) {
console.warn('[after-pack] ⚠️ Failed to patch https-proxy-agent:', err.message);

View File

@@ -0,0 +1,113 @@
#!/usr/bin/env zx
import 'zx/globals';
const ROOT_DIR = path.resolve(__dirname, '..');
const NODE_VERSION = '22.16.0';
const BASE_URL = `https://nodejs.org/dist/v${NODE_VERSION}`;
const OUTPUT_BASE = path.join(ROOT_DIR, 'resources', 'bin');
const TARGETS = {
'win32-x64': {
filename: `node-v${NODE_VERSION}-win-x64.zip`,
sourceDir: `node-v${NODE_VERSION}-win-x64`,
},
'win32-arm64': {
filename: `node-v${NODE_VERSION}-win-arm64.zip`,
sourceDir: `node-v${NODE_VERSION}-win-arm64`,
},
};
const PLATFORM_GROUPS = {
win: ['win32-x64', 'win32-arm64'],
};
async function setupTarget(id) {
const target = TARGETS[id];
if (!target) {
echo(chalk.yellow`⚠️ Target ${id} is not supported by this script.`);
return;
}
const targetDir = path.join(OUTPUT_BASE, id);
const tempDir = path.join(ROOT_DIR, 'temp_node_extract');
const archivePath = path.join(ROOT_DIR, target.filename);
const downloadUrl = `${BASE_URL}/${target.filename}`;
echo(chalk.blue`\n📦 Setting up Node.js for ${id}...`);
await fs.remove(targetDir);
await fs.remove(tempDir);
await fs.ensureDir(targetDir);
await fs.ensureDir(tempDir);
try {
echo`⬇️ Downloading: ${downloadUrl}`;
const response = await fetch(downloadUrl);
if (!response.ok) throw new Error(`Failed to download: ${response.statusText}`);
const buffer = await response.arrayBuffer();
await fs.writeFile(archivePath, Buffer.from(buffer));
echo`📂 Extracting...`;
if (os.platform() === 'win32') {
const { execFileSync } = await import('child_process');
const psCommand = `Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::ExtractToDirectory('${archivePath.replace(/'/g, "''")}', '${tempDir.replace(/'/g, "''")}')`;
execFileSync('powershell.exe', ['-NoProfile', '-Command', psCommand], { stdio: 'inherit' });
} else {
await $`unzip -q -o ${archivePath} -d ${tempDir}`;
}
const expectedNode = path.join(tempDir, target.sourceDir, 'node.exe');
const outputNode = path.join(targetDir, 'node.exe');
if (await fs.pathExists(expectedNode)) {
await fs.move(expectedNode, outputNode, { overwrite: true });
} else {
echo(chalk.yellow`🔍 node.exe not found in expected directory, searching...`);
const files = await glob('**/node.exe', { cwd: tempDir, absolute: true });
if (files.length > 0) {
await fs.move(files[0], outputNode, { overwrite: true });
} else {
throw new Error('Could not find node.exe in extracted files.');
}
}
echo(chalk.green`✅ Success: ${outputNode}`);
} finally {
await fs.remove(archivePath);
await fs.remove(tempDir);
}
}
const downloadAll = argv.all;
const platform = argv.platform;
if (downloadAll) {
echo(chalk.cyan`🌐 Downloading Node.js binaries for all Windows targets...`);
for (const id of Object.keys(TARGETS)) {
await setupTarget(id);
}
} else if (platform) {
const targets = PLATFORM_GROUPS[platform];
if (!targets) {
echo(chalk.red`❌ Unknown platform: ${platform}`);
echo(`Available platforms: ${Object.keys(PLATFORM_GROUPS).join(', ')}`);
process.exit(1);
}
echo(chalk.cyan`🎯 Downloading Node.js binaries for platform: ${platform}`);
for (const id of targets) {
await setupTarget(id);
}
} else {
const currentId = `${os.platform()}-${os.arch()}`;
if (TARGETS[currentId]) {
echo(chalk.cyan`💻 Detected Windows system: ${currentId}`);
await setupTarget(currentId);
} else {
echo(chalk.cyan`🎯 Defaulting to Windows multi-arch Node.js download`);
for (const id of PLATFORM_GROUPS.win) {
await setupTarget(id);
}
}
}
echo(chalk.green`\n🎉 Done!`);