fix(windows): run bundled openclaw CLI/TUI via node.exe to restore terminal input (#571)
This commit is contained in:
@@ -191,6 +191,7 @@ ClawXには、Electron、OpenClaw Gateway、またはTelegramなどのチャネ
|
||||
- プロキシ設定を保存すると、Electronのネットワーク設定が即座に再適用され、ゲートウェイが自動的に再起動されます。
|
||||
- ClawXはTelegramが有効な場合、プロキシをOpenClawのTelegramチャネル設定にも同期します。
|
||||
- **設定 → 詳細 → 開発者** では **OpenClaw Doctor** を実行でき、`openclaw doctor --json` の診断出力をアプリ内で確認できます。
|
||||
- Windows のパッケージ版では、同梱された `openclaw` CLI/TUI は端末入力を安定させるため、同梱の `node.exe` エントリーポイント経由で実行されます。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -195,6 +195,7 @@ Notes:
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -195,6 +195,7 @@ ClawX 内置了代理设置,适用于需要通过本地代理客户端访问
|
||||
- 保存代理设置后,Electron 网络层会立即重新应用代理,并自动重启 Gateway。
|
||||
- 如果启用了 Telegram,ClawX 还会把代理同步到 OpenClaw 的 Telegram 频道配置中。
|
||||
- 在 **设置 → 高级 → 开发者** 中,可以直接运行 **OpenClaw Doctor**,执行 `openclaw doctor --json` 并在应用内查看诊断输出。
|
||||
- 在 Windows 打包版本中,内置的 `openclaw` CLI/TUI 会通过随包分发的 `node.exe` 入口运行,以保证终端输入行为稳定。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -31,6 +31,12 @@ function quoteForPowerShell(value: string): string {
|
||||
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) ──────────────────────────────────
|
||||
|
||||
export function getOpenClawCliCommand(): string {
|
||||
@@ -69,7 +75,12 @@ export function getOpenClawCliCommand(): string {
|
||||
const cliDir = join(process.resourcesPath, 'cli');
|
||||
const cmdPath = join(cliDir, 'openclaw.cmd');
|
||||
if (existsSync(cmdPath)) {
|
||||
return quoteForPowerShell(cmdPath);
|
||||
return `& ${quoteForPowerShell(cmdPath)}`;
|
||||
}
|
||||
|
||||
const bundledNode = getPackagedWindowsNodePath();
|
||||
if (bundledNode) {
|
||||
return `& ${quoteForPowerShell(bundledNode)} ${quoteForPowerShell(entryPath)}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
"sharp"
|
||||
],
|
||||
"overrides": {
|
||||
"isbinaryfile": "^5.0.0"
|
||||
"isbinaryfile": "^5.0.0",
|
||||
"https-proxy-agent": "7.0.6"
|
||||
}
|
||||
},
|
||||
"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:linux": "zx scripts/download-bundled-uv.mjs --platform=linux",
|
||||
"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",
|
||||
"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: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",
|
||||
"release": "pnpm run uv:download && pnpm run package && electron-builder --publish always",
|
||||
"version:patch": "pnpm version patch",
|
||||
|
||||
48
pnpm-lock.yaml
generated
48
pnpm-lock.yaml
generated
@@ -6,6 +6,7 @@ settings:
|
||||
|
||||
overrides:
|
||||
isbinaryfile: ^5.0.0
|
||||
https-proxy-agent: 7.0.6
|
||||
|
||||
importers:
|
||||
|
||||
@@ -3271,18 +3272,10 @@ packages:
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
agent-base@6.0.2:
|
||||
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
|
||||
engines: {node: '>= 6.0.0'}
|
||||
|
||||
agent-base@7.1.4:
|
||||
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
agent-base@8.0.0:
|
||||
resolution: {integrity: sha512-QT8i0hCz6C/KQ+KTAbSNwCHDGdmUJl2tp2ZpNlGSWCfhUNVbYG2WLE3MdZGBAgXPV4GAvjGMxo+C1hroyxmZEg==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
ajv-formats@3.0.1:
|
||||
resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==}
|
||||
peerDependencies:
|
||||
@@ -4480,7 +4473,6 @@ packages:
|
||||
glob@11.1.0:
|
||||
resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==}
|
||||
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
|
||||
|
||||
glob@13.0.6:
|
||||
@@ -4617,18 +4609,10 @@ packages:
|
||||
resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==}
|
||||
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:
|
||||
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
https-proxy-agent@8.0.0:
|
||||
resolution: {integrity: sha512-YYeW+iCnAS3xhvj2dvVoWgsbca3RfQy/IlaNHHOtDmU0jMqPI9euIq3Y9BJETdxk16h9NHHCKqp/KB9nIMStCQ==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
i18next@25.8.11:
|
||||
resolution: {integrity: sha512-LZ32llTLGludnddjLoijHV7TbmVubU5eJnsWf8taiuM3jmSfUuvBLuyDeubJKS1yBjLBgb7As124M4KWNcBvpw==}
|
||||
peerDependencies:
|
||||
@@ -6498,7 +6482,6 @@ packages:
|
||||
tar@6.2.1:
|
||||
resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
|
||||
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:
|
||||
resolution: {integrity: sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==}
|
||||
@@ -6507,7 +6490,6 @@ packages:
|
||||
tar@7.5.4:
|
||||
resolution: {integrity: sha512-AN04xbWGrSTDmVwlI4/GTlIIwMFk/XEv7uL8aa57zuvRy6s4hdBed+lVq2fAZ89XDa7Us3ANXcE3Tvqvja1kTA==}
|
||||
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:
|
||||
resolution: {integrity: sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==}
|
||||
@@ -8075,7 +8057,7 @@ snapshots:
|
||||
'@discordjs/node-pre-gyp@0.4.5(encoding@0.1.13)':
|
||||
dependencies:
|
||||
detect-libc: 2.1.2
|
||||
https-proxy-agent: 5.0.1
|
||||
https-proxy-agent: 7.0.6
|
||||
make-dir: 3.1.0
|
||||
node-fetch: 2.7.0(encoding@0.1.13)
|
||||
nopt: 5.0.0
|
||||
@@ -10946,17 +10928,8 @@ snapshots:
|
||||
|
||||
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@8.0.0: {}
|
||||
|
||||
ajv-formats@3.0.1(ajv@8.18.0):
|
||||
optionalDependencies:
|
||||
ajv: 8.18.0
|
||||
@@ -12659,14 +12632,6 @@ snapshots:
|
||||
quick-lru: 5.1.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:
|
||||
dependencies:
|
||||
agent-base: 7.1.4
|
||||
@@ -12674,13 +12639,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- 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):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.6
|
||||
@@ -13911,7 +13869,7 @@ snapshots:
|
||||
file-type: 21.3.2
|
||||
grammy: 1.41.1(encoding@0.1.13)
|
||||
hono: 4.12.7
|
||||
https-proxy-agent: 8.0.0
|
||||
https-proxy-agent: 7.0.6
|
||||
ipaddr.js: 2.3.0
|
||||
jiti: 2.6.1
|
||||
json5: 2.2.3
|
||||
|
||||
@@ -14,4 +14,13 @@ case "$1" in
|
||||
esac
|
||||
|
||||
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" "$@"
|
||||
|
||||
@@ -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
|
||||
chcp 65001 >nul 2>&1
|
||||
|
||||
set ELECTRON_RUN_AS_NODE=1
|
||||
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%
|
||||
|
||||
if defined _CP chcp %_CP% >nul 2>&1
|
||||
|
||||
@@ -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);
|
||||
|
||||
113
scripts/download-bundled-node.mjs
Normal file
113
scripts/download-bundled-node.mjs
Normal 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!`);
|
||||
88
tests/unit/openclaw-cli.test.ts
Normal file
88
tests/unit/openclaw-cli.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user