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

@@ -191,6 +191,7 @@ ClawXには、Electron、OpenClaw Gateway、またはTelegramなどのチャネ
- プロキシ設定を保存すると、Electronのネットワーク設定が即座に再適用され、ゲートウェイが自動的に再起動されます。 - プロキシ設定を保存すると、Electronのネットワーク設定が即座に再適用され、ゲートウェイが自動的に再起動されます。
- ClawXはTelegramが有効な場合、プロキシをOpenClawのTelegramチャネル設定にも同期します。 - ClawXはTelegramが有効な場合、プロキシをOpenClawのTelegramチャネル設定にも同期します。
- **設定 → 詳細 → 開発者** では **OpenClaw Doctor** を実行でき、`openclaw doctor --json` の診断出力をアプリ内で確認できます。 - **設定 → 詳細 → 開発者** では **OpenClaw Doctor** を実行でき、`openclaw doctor --json` の診断出力をアプリ内で確認できます。
- Windows のパッケージ版では、同梱された `openclaw` CLI/TUI は端末入力を安定させるため、同梱の `node.exe` エントリーポイント経由で実行されます。
--- ---

View File

@@ -195,6 +195,7 @@ Notes:
- Saving proxy settings reapplies Electron networking immediately and restarts the Gateway automatically. - Saving proxy settings reapplies Electron networking immediately and restarts the Gateway automatically.
- ClawX also syncs the proxy to OpenClaw's Telegram channel config when Telegram is enabled. - ClawX also syncs the proxy to OpenClaw's Telegram channel config when Telegram is enabled.
- In **Settings → Advanced → Developer**, you can run **OpenClaw Doctor** to execute `openclaw doctor --json` and inspect the diagnostic output without leaving the app. - In **Settings → Advanced → Developer**, you can run **OpenClaw Doctor** to execute `openclaw doctor --json` and inspect the diagnostic output without leaving the app.
- On packaged Windows builds, the bundled `openclaw` CLI/TUI runs via the shipped `node.exe` entrypoint to keep terminal input behavior stable.
--- ---

View File

@@ -195,6 +195,7 @@ ClawX 内置了代理设置,适用于需要通过本地代理客户端访问
- 保存代理设置后Electron 网络层会立即重新应用代理,并自动重启 Gateway。 - 保存代理设置后Electron 网络层会立即重新应用代理,并自动重启 Gateway。
- 如果启用了 TelegramClawX 还会把代理同步到 OpenClaw 的 Telegram 频道配置中。 - 如果启用了 TelegramClawX 还会把代理同步到 OpenClaw 的 Telegram 频道配置中。
-**设置 → 高级 → 开发者** 中,可以直接运行 **OpenClaw Doctor**,执行 `openclaw doctor --json` 并在应用内查看诊断输出。 -**设置 → 高级 → 开发者** 中,可以直接运行 **OpenClaw Doctor**,执行 `openclaw doctor --json` 并在应用内查看诊断输出。
- 在 Windows 打包版本中,内置的 `openclaw` CLI/TUI 会通过随包分发的 `node.exe` 入口运行,以保证终端输入行为稳定。
--- ---

View File

@@ -31,6 +31,12 @@ function quoteForPowerShell(value: string): string {
return `'${value.replace(/'/g, "''")}'`; return `'${value.replace(/'/g, "''")}'`;
} }
function getPackagedWindowsNodePath(): string | null {
if (!app.isPackaged || process.platform !== 'win32') return null;
const nodePath = join(process.resourcesPath, 'bin', 'node.exe');
return existsSync(nodePath) ? nodePath : null;
}
// ── CLI command string (for display / copy) ────────────────────────────────── // ── CLI command string (for display / copy) ──────────────────────────────────
export function getOpenClawCliCommand(): string { export function getOpenClawCliCommand(): string {
@@ -69,7 +75,12 @@ export function getOpenClawCliCommand(): string {
const cliDir = join(process.resourcesPath, 'cli'); const cliDir = join(process.resourcesPath, 'cli');
const cmdPath = join(cliDir, 'openclaw.cmd'); const cmdPath = join(cliDir, 'openclaw.cmd');
if (existsSync(cmdPath)) { if (existsSync(cmdPath)) {
return quoteForPowerShell(cmdPath); return `& ${quoteForPowerShell(cmdPath)}`;
}
const bundledNode = getPackagedWindowsNodePath();
if (bundledNode) {
return `& ${quoteForPowerShell(bundledNode)} ${quoteForPowerShell(entryPath)}`;
} }
} }

View File

@@ -13,7 +13,8 @@
"sharp" "sharp"
], ],
"overrides": { "overrides": {
"isbinaryfile": "^5.0.0" "isbinaryfile": "^5.0.0",
"https-proxy-agent": "7.0.6"
} }
}, },
"description": "ClawX - Graphical AI Assistant based on OpenClaw", "description": "ClawX - Graphical AI Assistant based on OpenClaw",
@@ -40,11 +41,13 @@
"uv:download:win": "zx scripts/download-bundled-uv.mjs --platform=win", "uv:download:win": "zx scripts/download-bundled-uv.mjs --platform=win",
"uv:download:linux": "zx scripts/download-bundled-uv.mjs --platform=linux", "uv:download:linux": "zx scripts/download-bundled-uv.mjs --platform=linux",
"uv:download:all": "zx scripts/download-bundled-uv.mjs --all", "uv:download:all": "zx scripts/download-bundled-uv.mjs --all",
"node:download:win": "zx scripts/download-bundled-node.mjs --platform=win",
"prep:win-binaries": "pnpm run uv:download:win && pnpm run node:download:win",
"icons": "zx scripts/generate-icons.mjs", "icons": "zx scripts/generate-icons.mjs",
"package": "vite build && zx scripts/bundle-openclaw.mjs && zx scripts/bundle-openclaw-plugins.mjs && zx scripts/bundle-preinstalled-skills.mjs", "package": "vite build && zx scripts/bundle-openclaw.mjs && zx scripts/bundle-openclaw-plugins.mjs && zx scripts/bundle-preinstalled-skills.mjs",
"package:mac": "pnpm run package && electron-builder --mac --publish never", "package:mac": "pnpm run package && electron-builder --mac --publish never",
"package:mac:local": "SKIP_PREINSTALLED_SKILLS=1 pnpm run package && electron-builder --mac --publish never", "package:mac:local": "SKIP_PREINSTALLED_SKILLS=1 pnpm run package && electron-builder --mac --publish never",
"package:win": "pnpm run package && electron-builder --win --publish never", "package:win": "pnpm run prep:win-binaries && pnpm run package && electron-builder --win --publish never",
"package:linux": "pnpm run package && electron-builder --linux --publish never", "package:linux": "pnpm run package && electron-builder --linux --publish never",
"release": "pnpm run uv:download && pnpm run package && electron-builder --publish always", "release": "pnpm run uv:download && pnpm run package && electron-builder --publish always",
"version:patch": "pnpm version patch", "version:patch": "pnpm version patch",

48
pnpm-lock.yaml generated
View File

@@ -6,6 +6,7 @@ settings:
overrides: overrides:
isbinaryfile: ^5.0.0 isbinaryfile: ^5.0.0
https-proxy-agent: 7.0.6
importers: importers:
@@ -3271,18 +3272,10 @@ packages:
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
hasBin: true hasBin: true
agent-base@6.0.2:
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
engines: {node: '>= 6.0.0'}
agent-base@7.1.4: agent-base@7.1.4:
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
engines: {node: '>= 14'} engines: {node: '>= 14'}
agent-base@8.0.0:
resolution: {integrity: sha512-QT8i0hCz6C/KQ+KTAbSNwCHDGdmUJl2tp2ZpNlGSWCfhUNVbYG2WLE3MdZGBAgXPV4GAvjGMxo+C1hroyxmZEg==}
engines: {node: '>= 14'}
ajv-formats@3.0.1: ajv-formats@3.0.1:
resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==}
peerDependencies: peerDependencies:
@@ -4480,7 +4473,6 @@ packages:
glob@11.1.0: glob@11.1.0:
resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==}
engines: {node: 20 || >=22} engines: {node: 20 || >=22}
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
hasBin: true hasBin: true
glob@13.0.6: glob@13.0.6:
@@ -4617,18 +4609,10 @@ packages:
resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==}
engines: {node: '>=10.19.0'} engines: {node: '>=10.19.0'}
https-proxy-agent@5.0.1:
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
engines: {node: '>= 6'}
https-proxy-agent@7.0.6: https-proxy-agent@7.0.6:
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
engines: {node: '>= 14'} engines: {node: '>= 14'}
https-proxy-agent@8.0.0:
resolution: {integrity: sha512-YYeW+iCnAS3xhvj2dvVoWgsbca3RfQy/IlaNHHOtDmU0jMqPI9euIq3Y9BJETdxk16h9NHHCKqp/KB9nIMStCQ==}
engines: {node: '>= 14'}
i18next@25.8.11: i18next@25.8.11:
resolution: {integrity: sha512-LZ32llTLGludnddjLoijHV7TbmVubU5eJnsWf8taiuM3jmSfUuvBLuyDeubJKS1yBjLBgb7As124M4KWNcBvpw==} resolution: {integrity: sha512-LZ32llTLGludnddjLoijHV7TbmVubU5eJnsWf8taiuM3jmSfUuvBLuyDeubJKS1yBjLBgb7As124M4KWNcBvpw==}
peerDependencies: peerDependencies:
@@ -6498,7 +6482,6 @@ packages:
tar@6.2.1: tar@6.2.1:
resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
engines: {node: '>=10'} engines: {node: '>=10'}
deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
tar@7.5.11: tar@7.5.11:
resolution: {integrity: sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==} resolution: {integrity: sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==}
@@ -6507,7 +6490,6 @@ packages:
tar@7.5.4: tar@7.5.4:
resolution: {integrity: sha512-AN04xbWGrSTDmVwlI4/GTlIIwMFk/XEv7uL8aa57zuvRy6s4hdBed+lVq2fAZ89XDa7Us3ANXcE3Tvqvja1kTA==} resolution: {integrity: sha512-AN04xbWGrSTDmVwlI4/GTlIIwMFk/XEv7uL8aa57zuvRy6s4hdBed+lVq2fAZ89XDa7Us3ANXcE3Tvqvja1kTA==}
engines: {node: '>=18'} engines: {node: '>=18'}
deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
tar@7.5.9: tar@7.5.9:
resolution: {integrity: sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==} resolution: {integrity: sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==}
@@ -8075,7 +8057,7 @@ snapshots:
'@discordjs/node-pre-gyp@0.4.5(encoding@0.1.13)': '@discordjs/node-pre-gyp@0.4.5(encoding@0.1.13)':
dependencies: dependencies:
detect-libc: 2.1.2 detect-libc: 2.1.2
https-proxy-agent: 5.0.1 https-proxy-agent: 7.0.6
make-dir: 3.1.0 make-dir: 3.1.0
node-fetch: 2.7.0(encoding@0.1.13) node-fetch: 2.7.0(encoding@0.1.13)
nopt: 5.0.0 nopt: 5.0.0
@@ -10946,17 +10928,8 @@ snapshots:
acorn@8.16.0: {} acorn@8.16.0: {}
agent-base@6.0.2:
dependencies:
debug: 4.4.3
transitivePeerDependencies:
- supports-color
optional: true
agent-base@7.1.4: {} agent-base@7.1.4: {}
agent-base@8.0.0: {}
ajv-formats@3.0.1(ajv@8.18.0): ajv-formats@3.0.1(ajv@8.18.0):
optionalDependencies: optionalDependencies:
ajv: 8.18.0 ajv: 8.18.0
@@ -12659,14 +12632,6 @@ snapshots:
quick-lru: 5.1.1 quick-lru: 5.1.1
resolve-alpn: 1.2.1 resolve-alpn: 1.2.1
https-proxy-agent@5.0.1:
dependencies:
agent-base: 6.0.2
debug: 4.4.3
transitivePeerDependencies:
- supports-color
optional: true
https-proxy-agent@7.0.6: https-proxy-agent@7.0.6:
dependencies: dependencies:
agent-base: 7.1.4 agent-base: 7.1.4
@@ -12674,13 +12639,6 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
https-proxy-agent@8.0.0:
dependencies:
agent-base: 8.0.0
debug: 4.4.3
transitivePeerDependencies:
- supports-color
i18next@25.8.11(typescript@5.9.3): i18next@25.8.11(typescript@5.9.3):
dependencies: dependencies:
'@babel/runtime': 7.28.6 '@babel/runtime': 7.28.6
@@ -13911,7 +13869,7 @@ snapshots:
file-type: 21.3.2 file-type: 21.3.2
grammy: 1.41.1(encoding@0.1.13) grammy: 1.41.1(encoding@0.1.13)
hono: 4.12.7 hono: 4.12.7
https-proxy-agent: 8.0.0 https-proxy-agent: 7.0.6
ipaddr.js: 2.3.0 ipaddr.js: 2.3.0
jiti: 2.6.1 jiti: 2.6.1
json5: 2.2.3 json5: 2.2.3

View File

@@ -14,4 +14,13 @@ case "$1" in
esac esac
export OPENCLAW_EMBEDDED_IN="ClawX" export OPENCLAW_EMBEDDED_IN="ClawX"
ELECTRON_RUN_AS_NODE=1 exec "$INSTALL_DIR/ClawX.exe" "$INSTALL_DIR/resources/openclaw/openclaw.mjs" "$@" NODE_EXE="$INSTALL_DIR/resources/bin/node.exe"
OPENCLAW_ENTRY="$INSTALL_DIR/resources/openclaw/openclaw.mjs"
if [ -f "$NODE_EXE" ]; then
if "$NODE_EXE" -e 'const [maj,min]=process.versions.node.split(".").map(Number);process.exit((maj>22||maj===22&&min>=16)?0:1)' >/dev/null 2>&1; then
exec "$NODE_EXE" "$OPENCLAW_ENTRY" "$@"
fi
fi
ELECTRON_RUN_AS_NODE=1 exec "$INSTALL_DIR/ClawX.exe" "$OPENCLAW_ENTRY" "$@"

View File

@@ -15,9 +15,22 @@ rem on non-English Windows (e.g. Chinese CP936). Save the previous codepage to r
for /f "tokens=2 delims=:." %%a in ('chcp') do set /a "_CP=%%a" 2>nul for /f "tokens=2 delims=:." %%a in ('chcp') do set /a "_CP=%%a" 2>nul
chcp 65001 >nul 2>&1 chcp 65001 >nul 2>&1
set ELECTRON_RUN_AS_NODE=1
set OPENCLAW_EMBEDDED_IN=ClawX set OPENCLAW_EMBEDDED_IN=ClawX
"%~dp0..\..\ClawX.exe" "%~dp0..\openclaw\openclaw.mjs" %* set "NODE_EXE=%~dp0..\bin\node.exe"
set "OPENCLAW_ENTRY=%~dp0..\openclaw\openclaw.mjs"
set "_USE_BUNDLED_NODE=0"
if exist "%NODE_EXE%" (
"%NODE_EXE%" -e "const [maj,min]=process.versions.node.split('.').map(Number);process.exit((maj>22||maj===22&&min>=16)?0:1)" >nul 2>&1
if not errorlevel 1 set "_USE_BUNDLED_NODE=1"
)
if "%_USE_BUNDLED_NODE%"=="1" (
"%NODE_EXE%" "%OPENCLAW_ENTRY%" %*
) else (
set ELECTRON_RUN_AS_NODE=1
"%~dp0..\..\ClawX.exe" "%OPENCLAW_ENTRY%" %*
)
set _EXIT=%ERRORLEVEL% set _EXIT=%ERRORLEVEL%
if defined _CP chcp %_CP% >nul 2>&1 if defined _CP chcp %_CP% >nul 2>&1

View File

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

View File

@@ -0,0 +1,88 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
const originalPlatform = process.platform;
const originalResourcesPath = process.resourcesPath;
const {
mockExistsSync,
mockIsPackagedGetter,
} = vi.hoisted(() => ({
mockExistsSync: vi.fn<(path: string) => boolean>(),
mockIsPackagedGetter: { value: false },
}));
function setPlatform(platform: string) {
Object.defineProperty(process, 'platform', { value: platform, writable: true });
}
vi.mock('node:fs', async () => {
const actual = await vi.importActual<typeof import('node:fs')>('node:fs');
return {
...actual,
existsSync: mockExistsSync,
default: {
...actual,
existsSync: mockExistsSync,
},
};
});
vi.mock('electron', () => ({
app: {
get isPackaged() {
return mockIsPackagedGetter.value;
},
},
}));
vi.mock('@electron/utils/paths', () => ({
getOpenClawDir: () => '/tmp/openclaw',
getOpenClawEntryPath: () => 'C:\\Program Files\\ClawX\\resources\\openclaw\\openclaw.mjs',
}));
describe('getOpenClawCliCommand (Windows packaged)', () => {
beforeEach(() => {
vi.resetModules();
vi.clearAllMocks();
setPlatform('win32');
mockIsPackagedGetter.value = true;
Object.defineProperty(process, 'resourcesPath', {
value: 'C:\\Program Files\\ClawX\\resources',
configurable: true,
writable: true,
});
});
afterEach(() => {
Object.defineProperty(process, 'platform', { value: originalPlatform, writable: true });
Object.defineProperty(process, 'resourcesPath', {
value: originalResourcesPath,
configurable: true,
writable: true,
});
});
it('prefers bundled node.exe when present', async () => {
mockExistsSync.mockImplementation((p: string) => /[\\/]cli[\\/]openclaw\.cmd$/i.test(p) || /[\\/]bin[\\/]node\.exe$/i.test(p));
const { getOpenClawCliCommand } = await import('@electron/utils/openclaw-cli');
expect(getOpenClawCliCommand()).toBe(
"& 'C:\\Program Files\\ClawX\\resources/cli/openclaw.cmd'",
);
});
it('falls back to bundled node.exe when openclaw.cmd is missing', async () => {
mockExistsSync.mockImplementation((p: string) => /[\\/]bin[\\/]node\.exe$/i.test(p));
const { getOpenClawCliCommand } = await import('@electron/utils/openclaw-cli');
expect(getOpenClawCliCommand()).toBe(
"& 'C:\\Program Files\\ClawX\\resources/bin/node.exe' 'C:\\Program Files\\ClawX\\resources\\openclaw\\openclaw.mjs'",
);
});
it('falls back to ELECTRON_RUN_AS_NODE command when wrappers are missing', async () => {
mockExistsSync.mockReturnValue(false);
const { getOpenClawCliCommand } = await import('@electron/utils/openclaw-cli');
const command = getOpenClawCliCommand();
expect(command.startsWith('$env:ELECTRON_RUN_AS_NODE=1; & ')).toBe(true);
expect(command.endsWith("'C:\\Program Files\\ClawX\\resources\\openclaw\\openclaw.mjs'")).toBe(true);
});
});