From 90e02636c7553b8d93bf41aef9eada06317beabb Mon Sep 17 00:00:00 2001
From: DEV_DSW <562304744@qq.com>
Date: Mon, 27 Apr 2026 14:21:23 +0800
Subject: [PATCH] feat: update preinstalled skills and CLI scripts, enhance
packaging process, and add configuration for Windows and macOS
---
.../.preinstalled-lock.json | 22 +-
dist/index.html | 4 +-
docs/Desktop-Packaging-Workflow.md | 344 ++++++++++++++++++
electron-builder.yml | 9 +
package.json | 4 +-
resources/cli/posix/openclaw | 54 +++
resources/cli/win32/openclaw | 26 ++
resources/cli/win32/openclaw.cmd | 37 ++
resources/cli/win32/update-user-path.ps1 | 150 ++++++++
scripts/bundle-preinstalled-skills.mjs | 3 +
scripts/configure-zx-shell.mjs | 7 +
scripts/download-bundled-node.mjs | 5 +-
scripts/download-bundled-uv.mjs | 3 +
scripts/generate-icons.mjs | 3 +
scripts/patch-electron-builder-nsis.mjs | 75 ++++
scripts/prepare-preinstalled-skills-dev.mjs | 3 +
16 files changed, 733 insertions(+), 16 deletions(-)
create mode 100644 docs/Desktop-Packaging-Workflow.md
create mode 100644 resources/cli/posix/openclaw
create mode 100644 resources/cli/win32/openclaw
create mode 100644 resources/cli/win32/openclaw.cmd
create mode 100644 resources/cli/win32/update-user-path.ps1
create mode 100644 scripts/configure-zx-shell.mjs
create mode 100644 scripts/patch-electron-builder-nsis.mjs
diff --git a/build/preinstalled-skills/.preinstalled-lock.json b/build/preinstalled-skills/.preinstalled-lock.json
index 5c52695..5a6df94 100644
--- a/build/preinstalled-skills/.preinstalled-lock.json
+++ b/build/preinstalled-skills/.preinstalled-lock.json
@@ -1,45 +1,45 @@
{
- "generatedAt": "2026-04-19T12:02:30.404Z",
+ "generatedAt": "2026-04-27T03:36:36.328Z",
"skills": [
{
"slug": "pdf",
- "version": "2c7ec5e78b8e5d43ea02e90bb8826f6b9f147b0c",
+ "version": "5128e1865d670f5d6c9cef000e6dfc4e951fb5b9",
"repo": "anthropics/skills",
"repoPath": "skills/pdf",
"ref": "main",
- "commit": "2c7ec5e78b8e5d43ea02e90bb8826f6b9f147b0c"
+ "commit": "5128e1865d670f5d6c9cef000e6dfc4e951fb5b9"
},
{
"slug": "xlsx",
- "version": "2c7ec5e78b8e5d43ea02e90bb8826f6b9f147b0c",
+ "version": "5128e1865d670f5d6c9cef000e6dfc4e951fb5b9",
"repo": "anthropics/skills",
"repoPath": "skills/xlsx",
"ref": "main",
- "commit": "2c7ec5e78b8e5d43ea02e90bb8826f6b9f147b0c"
+ "commit": "5128e1865d670f5d6c9cef000e6dfc4e951fb5b9"
},
{
"slug": "docx",
- "version": "2c7ec5e78b8e5d43ea02e90bb8826f6b9f147b0c",
+ "version": "5128e1865d670f5d6c9cef000e6dfc4e951fb5b9",
"repo": "anthropics/skills",
"repoPath": "skills/docx",
"ref": "main",
- "commit": "2c7ec5e78b8e5d43ea02e90bb8826f6b9f147b0c"
+ "commit": "5128e1865d670f5d6c9cef000e6dfc4e951fb5b9"
},
{
"slug": "pptx",
- "version": "2c7ec5e78b8e5d43ea02e90bb8826f6b9f147b0c",
+ "version": "5128e1865d670f5d6c9cef000e6dfc4e951fb5b9",
"repo": "anthropics/skills",
"repoPath": "skills/pptx",
"ref": "main",
- "commit": "2c7ec5e78b8e5d43ea02e90bb8826f6b9f147b0c"
+ "commit": "5128e1865d670f5d6c9cef000e6dfc4e951fb5b9"
},
{
"slug": "find-skills",
- "version": "bc21a37a12b90fcb5aec051c91baf5b227b704b1",
+ "version": "5516b8ad07393f35af4b50238b9d3f7cceca5f1e",
"repo": "vercel-labs/skills",
"repoPath": "skills/find-skills",
"ref": "main",
- "commit": "bc21a37a12b90fcb5aec051c91baf5b227b704b1"
+ "commit": "5516b8ad07393f35af4b50238b9d3f7cceca5f1e"
},
{
"slug": "self-improving-agent",
diff --git a/dist/index.html b/dist/index.html
index dc28771..be480ed 100644
--- a/dist/index.html
+++ b/dist/index.html
@@ -8,8 +8,8 @@
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: http://8.138.234.141 https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com; connect-src 'self' http://8.138.234.141 https://api.iconify.design wss://onefeel.brother7.cn"
/>
-
-
+
+
diff --git a/docs/Desktop-Packaging-Workflow.md b/docs/Desktop-Packaging-Workflow.md
new file mode 100644
index 0000000..0cd14f3
--- /dev/null
+++ b/docs/Desktop-Packaging-Workflow.md
@@ -0,0 +1,344 @@
+# 桌面端打包流程
+
+本文用于梳理 `zn-ai` 当前仓库的 Windows / macOS 打包流程,说明应该先执行哪个脚本、每个脚本会自动带哪些前置步骤,以及常见使用场景。
+
+## 1. 先记结论
+
+如果你只是想正常出包,不需要手动先跑 `bundle-openclaw`、`bundle:preinstalled-skills`、`uv:download:*`。
+
+直接按目标平台执行:
+
+- Windows 安装包:`pnpm run package:win`
+- mac 双架构包:`pnpm run package:mac`
+- mac Intel 单架构包:`pnpm run package:mac:x64`
+- mac Apple Silicon 单架构包:`pnpm run package:mac:arm64`
+
+也就是说:
+
+- 要打 Windows 包,优先跑 `package:win`
+- 要打 mac 包,优先跑 `package:mac` 或具体架构脚本
+- `package` 只是“准备打包产物”,不是最终安装包命令
+- `build` 是“当前宿主机默认 electron-builder 打包”,不如平台脚本直观,日常不建议优先用
+
+## 2. 推荐执行顺序
+
+### 2.1 第一次打包或切分支后
+
+1. `pnpm install`
+2. `pnpm typecheck`
+3. 按目标平台执行对应脚本
+
+推荐命令:
+
+```powershell
+pnpm install
+pnpm typecheck
+pnpm run package:win
+```
+
+或:
+
+```powershell
+pnpm install
+pnpm typecheck
+pnpm run package:mac
+```
+
+### 2.2 日常出 Windows 包
+
+```powershell
+pnpm run package:win
+```
+
+### 2.3 日常出 mac 包
+
+如果希望一次出 x64 + arm64:
+
+```powershell
+pnpm run package:mac
+```
+
+如果只想出单一架构:
+
+```powershell
+pnpm run package:mac:x64
+```
+
+或:
+
+```powershell
+pnpm run package:mac:arm64
+```
+
+## 3. 各脚本职责
+
+### 3.1 `pnpm run package`
+
+这是“打包前准备阶段”,会自动执行:
+
+1. `prepackage`
+2. `vite build`
+3. `node scripts/bundle-openclaw.mjs`
+4. `zx scripts/bundle-preinstalled-skills.mjs`
+
+所以它会完成:
+
+- 检查并补齐平台所需的 bundled runtime binaries
+- 生成 `dist` 和 `dist-electron`
+- 生成 `build/openclaw`
+- 生成 `build/preinstalled-skills`
+
+但是它不会直接产出最终安装包。
+
+### 3.2 `pnpm run package:win`
+
+等价于:
+
+```powershell
+pnpm run package
+electron-builder --win --publish never
+```
+
+用途:
+
+- 产出 Windows NSIS 安装包
+- 当前配置只打 `x64`
+
+### 3.3 `pnpm run package:mac`
+
+等价于:
+
+```powershell
+pnpm run package
+electron-builder --mac --publish never
+```
+
+用途:
+
+- 产出 macOS 包
+- 当前配置目标包含 `dmg` 和 `zip`
+- 当前配置默认覆盖 `x64` 和 `arm64`
+
+### 3.4 `pnpm run build`
+
+等价于:
+
+```powershell
+pnpm run package
+electron-builder
+```
+
+特点:
+
+- 使用 `electron-builder.yml` 默认目标
+- 更适合“按宿主机默认配置整体打一遍”
+- 不如 `package:win` / `package:mac` 明确
+
+## 4. 打包时自动发生的事
+
+### 4.1 `prepackage`
+
+`prepackage` 会自动执行:
+
+```powershell
+node scripts/ensure-bundled-runtime-binaries.mjs
+```
+
+这一步会按当前平台检查 `resources/bin` 下的运行时文件是否齐全。
+
+当前规则:
+
+- Windows:检查 `uv.exe` 和 `node.exe`
+- macOS:检查 `uv`
+- Linux:检查 `uv`
+
+如果缺失,会自动调用对应下载脚本:
+
+- Windows:`uv:download:win`、`node:download:win`
+- macOS:`uv:download:mac`
+- Linux:`uv:download:linux`
+
+因此正常情况下,你不需要手动先跑这些下载脚本。
+
+### 4.2 `afterPack`
+
+`electron-builder` 打包后会自动执行:
+
+```powershell
+scripts/after-pack.cjs
+```
+
+它会继续完成这些动作:
+
+- 把 `resources/bin//...` 平铺复制到安装包里的 `resources/bin/`
+- 把 `electron/scripts` 处理到包内
+- 补拷 `playwright`、`playwright-core`、`chromium-bidi`、`bytenode`
+- 补拷 `build/openclaw`
+- 清理不必要的开发文件,缩小包体
+
+所以如果你是正常走 `package:win` / `package:mac`,这些都已经自动串好了。
+
+## 5. 什么时候手动跑单独脚本
+
+### 5.1 手动预拉 Windows 运行时
+
+如果你只是想提前把 Windows 运行时二进制准备好,可以执行:
+
+```powershell
+pnpm run prep:win-binaries
+```
+
+它会跑:
+
+```powershell
+pnpm run uv:download:win
+pnpm run node:download:win
+```
+
+适合场景:
+
+- 网络较慢,想在正式打包前先把依赖下载好
+- 排查 `resources/bin/win32-*` 是否齐全
+
+### 5.2 手动更新预装 skills
+
+如果你只想刷新预装 skills,不是正式出包,可以执行:
+
+```powershell
+pnpm run bundle:preinstalled-skills
+```
+
+### 5.3 手动更新 OpenClaw 运行时
+
+如果你只想刷新 `build/openclaw`,可以执行:
+
+```powershell
+pnpm run bundle:openclaw
+```
+
+## 6. 产物位置
+
+最终安装包 / 压缩包输出目录:
+
+```text
+release/
+```
+
+打包中间产物目录:
+
+```text
+dist/
+dist-electron/
+build/openclaw/
+build/preinstalled-skills/
+```
+
+## 7. Windows / mac 实操建议
+
+### 7.1 Windows
+
+推荐最简流程:
+
+```powershell
+pnpm install
+pnpm typecheck
+pnpm run package:win
+```
+
+说明:
+
+- 当前 `electron-builder.yml` 的 Windows 目标是 `nsis`
+- 当前只配置了 `x64`
+
+### 7.2 macOS
+
+推荐最简流程:
+
+```powershell
+pnpm install
+pnpm typecheck
+pnpm run package:mac
+```
+
+如果只打单架构:
+
+```powershell
+pnpm run package:mac:x64
+```
+
+或:
+
+```powershell
+pnpm run package:mac:arm64
+```
+
+如果只是本地快速验包,不追求最大压缩率:
+
+```powershell
+pnpm run package:mac:fast
+```
+
+如果想保留 `afterPack` 清理前的更多内容,便于本地排查:
+
+```powershell
+pnpm run package:mac:local
+```
+
+说明:
+
+- `package:mac:local` 会设置 `SKIP_AFTERPACK_CLEANUP=1`
+- 它不是跳过打包,而是跳过 `afterPack` 里的清理步骤
+
+## 8. 常见误区
+
+### 误区 1:先手动跑 `bundle-openclaw` 才能打包
+
+不是必须。
+
+因为 `package` 已经会自动执行:
+
+- `node scripts/bundle-openclaw.mjs`
+- `zx scripts/bundle-preinstalled-skills.mjs`
+
+### 误区 2:先手动跑 `uv:download:win` 才能打 Windows 包
+
+也不是必须。
+
+因为 `prepackage` 会自动执行 `ensure-bundled-runtime-binaries.mjs`,缺失时会自动下载。
+
+### 误区 3:`package` 就是最终出包命令
+
+不是。
+
+`package` 只做准备,不直接生成最终安装包;真正平台出包应使用:
+
+- `package:win`
+- `package:mac`
+- `package:mac:x64`
+- `package:mac:arm64`
+
+## 9. 建议你平时直接记住的命令
+
+### Windows
+
+```powershell
+pnpm run package:win
+```
+
+### macOS
+
+```powershell
+pnpm run package:mac
+```
+
+### 只做前置产物准备
+
+```powershell
+pnpm run package
+```
+
+### 只想检查类型
+
+```powershell
+pnpm typecheck
+```
diff --git a/electron-builder.yml b/electron-builder.yml
index a45c99f..3e95341 100644
--- a/electron-builder.yml
+++ b/electron-builder.yml
@@ -48,6 +48,9 @@ afterPack: ./scripts/after-pack.cjs
# macOS Configuration
mac:
+ extraResources:
+ - from: resources/cli/posix/
+ to: cli/
category: public.app-category.productivity
icon: resources/icons/icon.icns
target:
@@ -86,6 +89,9 @@ dmg:
win:
verifyUpdateCodeSignature: false
signAndEditExecutable: false
+ extraResources:
+ - from: resources/cli/win32/
+ to: cli/
icon: resources/icons/icon.ico
target:
- target: nsis
@@ -108,6 +114,9 @@ nsis:
# Linux Configuration
linux:
+ extraResources:
+ - from: resources/cli/posix/
+ to: cli/
icon: resources/icons/
target:
- target: AppImage
diff --git a/package.json b/package.json
index b93dba8..5cf2d63 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,7 @@
"main": "dist-electron/main/main.js",
"scripts": {
"init": "pnpm install && pnpm run uv:download",
- "prepackage": "node scripts/ensure-bundled-runtime-binaries.mjs",
+ "prepackage": "node scripts/ensure-bundled-runtime-binaries.mjs && node scripts/patch-electron-builder-nsis.mjs",
"predev": "zx scripts/prepare-preinstalled-skills-dev.mjs",
"prestart": "zx scripts/prepare-preinstalled-skills-dev.mjs",
"dev": "vite",
@@ -40,7 +40,7 @@
"generate-prod-entry": "node build/scripts/generateProdEntry.js",
"clean": "node build/scripts/clean.js",
"build:encrypt": "pnpm run clean && pnpm run build:vite && pnpm run package",
- "postinstall": "electron-builder install-app-deps"
+ "postinstall": "electron-builder install-app-deps && node scripts/patch-electron-builder-nsis.mjs"
},
"keywords": [],
"author": {
diff --git a/resources/cli/posix/openclaw b/resources/cli/posix/openclaw
new file mode 100644
index 0000000..f176c74
--- /dev/null
+++ b/resources/cli/posix/openclaw
@@ -0,0 +1,54 @@
+#!/bin/sh
+# OpenClaw CLI - managed by NIANXX
+# Do not edit manually. Regenerated on NIANXX updates.
+
+# Resolve the real path of this script (follow symlinks)
+SCRIPT="$0"
+while [ -L "$SCRIPT" ]; do
+ SCRIPT_DIR="$(cd -P "$(dirname "$SCRIPT")" && pwd)"
+ SCRIPT="$(readlink "$SCRIPT")"
+ [ "${SCRIPT#/}" = "$SCRIPT" ] && SCRIPT="$SCRIPT_DIR/$SCRIPT"
+done
+SCRIPT_DIR="$(cd -P "$(dirname "$SCRIPT")" && pwd)"
+
+if [ "$(uname)" = "Darwin" ]; then
+ # macOS: .app/Contents/Resources/cli/openclaw
+ # SCRIPT_DIR = .../Contents/Resources/cli
+ CONTENTS_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
+ ELECTRON="$CONTENTS_DIR/MacOS/NIANXX"
+ CLI="$CONTENTS_DIR/Resources/openclaw/openclaw.mjs"
+else
+ # Linux: /opt/NIANXX/resources/cli/openclaw
+ # SCRIPT_DIR = .../resources/cli
+ INSTALL_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
+ CLI="$INSTALL_DIR/resources/openclaw/openclaw.mjs"
+
+ if [ -x "$INSTALL_DIR/nianxx" ]; then
+ ELECTRON="$INSTALL_DIR/nianxx"
+ elif [ -x "$INSTALL_DIR/NIANXX" ]; then
+ ELECTRON="$INSTALL_DIR/NIANXX"
+ else
+ ELECTRON="$INSTALL_DIR/zn-ai"
+ fi
+fi
+
+if [ ! -f "$ELECTRON" ]; then
+ echo "Error: NIANXX executable not found at $ELECTRON" >&2
+ echo "Please reinstall NIANXX or remove this script: $0" >&2
+ exit 1
+fi
+
+case "$1" in
+ update)
+ echo "openclaw is managed by NIANXX (bundled version)."
+ echo ""
+ echo "To update openclaw, update NIANXX:"
+ echo " Open NIANXX > Settings > Check for Updates"
+ echo ""
+ ELECTRON_RUN_AS_NODE=1 "$ELECTRON" "$CLI" --version 2>/dev/null || true
+ exit 0
+ ;;
+esac
+
+export OPENCLAW_EMBEDDED_IN="NIANXX"
+ELECTRON_RUN_AS_NODE=1 exec "$ELECTRON" "$CLI" "$@"
diff --git a/resources/cli/win32/openclaw b/resources/cli/win32/openclaw
new file mode 100644
index 0000000..1464adf
--- /dev/null
+++ b/resources/cli/win32/openclaw
@@ -0,0 +1,26 @@
+#!/bin/sh
+# OpenClaw CLI wrapper for Git Bash / MSYS2 on Windows
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+INSTALL_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
+
+case "$1" in
+ update)
+ echo "openclaw is managed by NIANXX (bundled version)."
+ echo ""
+ echo "To update openclaw, update NIANXX:"
+ echo " Open NIANXX > Settings > Check for Updates"
+ exit 0
+ ;;
+esac
+
+export OPENCLAW_EMBEDDED_IN="NIANXX"
+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/NIANXX.exe" "$OPENCLAW_ENTRY" "$@"
diff --git a/resources/cli/win32/openclaw.cmd b/resources/cli/win32/openclaw.cmd
new file mode 100644
index 0000000..939f36e
--- /dev/null
+++ b/resources/cli/win32/openclaw.cmd
@@ -0,0 +1,37 @@
+@echo off
+setlocal
+
+if /i "%1"=="update" (
+ echo openclaw is managed by NIANXX ^(bundled version^).
+ echo.
+ echo To update openclaw, update NIANXX:
+ echo Open NIANXX ^> Settings ^> Check for Updates
+ exit /b 0
+)
+
+rem Switch console to UTF-8 so Unicode box-drawing and CJK text render correctly
+rem on non-English Windows (e.g. Chinese CP936). Save the previous codepage to restore later.
+for /f "tokens=2 delims=:." %%a in ('chcp') do set /a "_CP=%%a" 2>nul
+chcp 65001 >nul 2>&1
+
+set OPENCLAW_EMBEDDED_IN=NIANXX
+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..\..\NIANXX.exe" "%OPENCLAW_ENTRY%" %*
+)
+set _EXIT=%ERRORLEVEL%
+
+if defined _CP chcp %_CP% >nul 2>&1
+
+endlocal & exit /b %_EXIT%
diff --git a/resources/cli/win32/update-user-path.ps1 b/resources/cli/win32/update-user-path.ps1
new file mode 100644
index 0000000..3b5204b
--- /dev/null
+++ b/resources/cli/win32/update-user-path.ps1
@@ -0,0 +1,150 @@
+param(
+ [Parameter(Mandatory = $true)]
+ [ValidateSet('add', 'remove')]
+ [string]$Action,
+
+ [Parameter(Mandatory = $true)]
+ [string]$CliDir
+)
+
+$ErrorActionPreference = 'Stop'
+
+function Get-UserPathRegistryValue {
+ $raw = [Environment]::GetEnvironmentVariable('Path', 'User')
+ $kind = [Microsoft.Win32.RegistryValueKind]::ExpandString
+
+ try {
+ $key = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Environment', $false)
+ if ($null -ne $key) {
+ try {
+ $stored = $key.GetValue('Path', $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
+ if ($null -ne $stored) {
+ $raw = [string]$stored
+ }
+ } catch {
+ # Fallback to Environment API value
+ }
+
+ try {
+ $kind = $key.GetValueKind('Path')
+ } catch {
+ # Keep default ExpandString
+ }
+ $key.Close()
+ }
+ } catch {
+ # Fallback to Environment API value
+ }
+
+ return @{
+ Raw = $raw
+ Kind = $kind
+ }
+}
+
+function Normalize-PathEntry {
+ param([string]$Value)
+
+ if ([string]::IsNullOrWhiteSpace($Value)) {
+ return ''
+ }
+
+ return $Value.Trim().Trim('"').TrimEnd('\').ToLowerInvariant()
+}
+
+$pathMeta = Get-UserPathRegistryValue
+$current = $pathMeta.Raw
+$entries = @()
+if (-not [string]::IsNullOrWhiteSpace($current)) {
+ $entries = $current -split ';' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
+}
+
+$target = Normalize-PathEntry $CliDir
+$seen = [System.Collections.Generic.HashSet[string]]::new()
+$nextEntries = New-Object System.Collections.Generic.List[string]
+
+foreach ($entry in $entries) {
+ $normalized = Normalize-PathEntry $entry
+ if ([string]::IsNullOrWhiteSpace($normalized)) {
+ continue
+ }
+
+ if ($normalized -eq $target) {
+ continue
+ }
+
+ if ($seen.Add($normalized)) {
+ $nextEntries.Add($entry.Trim().Trim('"'))
+ }
+}
+
+$status = 'already-present'
+if ($Action -eq 'add') {
+ if ($seen.Add($target)) {
+ $nextEntries.Add($CliDir)
+ $status = 'updated'
+ }
+} elseif ($entries.Count -ne $nextEntries.Count) {
+ $status = 'updated'
+}
+
+$isLikelyCorruptedWrite = (
+ $Action -eq 'add' -and
+ $entries.Count -gt 1 -and
+ $nextEntries.Count -le 1
+)
+if ($isLikelyCorruptedWrite) {
+ throw "Refusing to rewrite user PATH: input had $($entries.Count) entries but output has $($nextEntries.Count)."
+}
+
+$newPath = if ($nextEntries.Count -eq 0) { $null } else { $nextEntries -join ';' }
+try {
+ $key = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Environment', $true)
+ if ($null -eq $key) {
+ throw 'Unable to open HKCU\Environment for write.'
+ }
+
+ if ([string]::IsNullOrWhiteSpace($newPath)) {
+ $key.DeleteValue('Path', $false)
+ } else {
+ $kind = if ($pathMeta.Kind -eq [Microsoft.Win32.RegistryValueKind]::String) {
+ [Microsoft.Win32.RegistryValueKind]::String
+ } else {
+ [Microsoft.Win32.RegistryValueKind]::ExpandString
+ }
+ $key.SetValue('Path', $newPath, $kind)
+ }
+ $key.Close()
+} catch {
+ throw "Failed to write HKCU\\Environment\\Path: $($_.Exception.Message)"
+}
+
+try {
+ Add-Type -Namespace OpenClaw -Name NativeMethods -MemberDefinition @"
+[System.Runtime.InteropServices.DllImport("user32.dll", SetLastError = true, CharSet = System.Runtime.InteropServices.CharSet.Auto)]
+public static extern System.IntPtr SendMessageTimeout(
+ System.IntPtr hWnd,
+ int Msg,
+ System.IntPtr wParam,
+ string lParam,
+ int fuFlags,
+ int uTimeout,
+ out System.IntPtr lpdwResult
+);
+"@
+
+ $result = [IntPtr]::Zero
+ [OpenClaw.NativeMethods]::SendMessageTimeout(
+ [IntPtr]0xffff,
+ 0x001A,
+ [IntPtr]::Zero,
+ 'Environment',
+ 0x0002,
+ 5000,
+ [ref]$result
+ ) | Out-Null
+} catch {
+ Write-Warning "PATH updated but failed to broadcast environment change: $($_.Exception.Message)"
+}
+
+Write-Output $status
diff --git a/scripts/bundle-preinstalled-skills.mjs b/scripts/bundle-preinstalled-skills.mjs
index 96a0faf..38a3fc0 100644
--- a/scripts/bundle-preinstalled-skills.mjs
+++ b/scripts/bundle-preinstalled-skills.mjs
@@ -1,10 +1,13 @@
#!/usr/bin/env zx
import 'zx/globals';
+import { configureZxShellForPlatform } from './configure-zx-shell.mjs';
import { readFileSync, existsSync, mkdirSync, rmSync, cpSync, writeFileSync } from 'node:fs';
import { join, dirname, basename } from 'node:path';
import { fileURLToPath } from 'node:url';
+configureZxShellForPlatform();
+
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, '..');
const MANIFEST_PATH = join(ROOT, 'resources', 'skills', 'preinstalled-manifest.json');
diff --git a/scripts/configure-zx-shell.mjs b/scripts/configure-zx-shell.mjs
new file mode 100644
index 0000000..8779b63
--- /dev/null
+++ b/scripts/configure-zx-shell.mjs
@@ -0,0 +1,7 @@
+import { usePowerShell } from 'zx';
+
+export function configureZxShellForPlatform() {
+ if (process.platform === 'win32') {
+ usePowerShell();
+ }
+}
diff --git a/scripts/download-bundled-node.mjs b/scripts/download-bundled-node.mjs
index f778bc0..0cb5a1a 100644
--- a/scripts/download-bundled-node.mjs
+++ b/scripts/download-bundled-node.mjs
@@ -1,6 +1,9 @@
#!/usr/bin/env zx
import 'zx/globals';
+import { configureZxShellForPlatform } from './configure-zx-shell.mjs';
+
+configureZxShellForPlatform();
const ROOT_DIR = path.resolve(__dirname, '..');
const NODE_VERSION = '22.16.0';
@@ -130,4 +133,4 @@ try {
echo(chalk.yellow` Packaging will continue without external Node.js binary.`);
// Exit with code 0 to allow packaging to continue
process.exit(0);
-}
\ No newline at end of file
+}
diff --git a/scripts/download-bundled-uv.mjs b/scripts/download-bundled-uv.mjs
index 51aa0ca..169c931 100644
--- a/scripts/download-bundled-uv.mjs
+++ b/scripts/download-bundled-uv.mjs
@@ -1,6 +1,9 @@
#!/usr/bin/env zx
import 'zx/globals';
+import { configureZxShellForPlatform } from './configure-zx-shell.mjs';
+
+configureZxShellForPlatform();
const ROOT_DIR = path.resolve(__dirname, '..');
const UV_VERSION = '0.10.0';
diff --git a/scripts/generate-icons.mjs b/scripts/generate-icons.mjs
index 6585ec5..671ae61 100644
--- a/scripts/generate-icons.mjs
+++ b/scripts/generate-icons.mjs
@@ -1,10 +1,13 @@
#!/usr/bin/env zx
import 'zx/globals';
+import { configureZxShellForPlatform } from './configure-zx-shell.mjs';
import sharp from 'sharp';
import png2icons from 'png2icons';
import { fileURLToPath } from 'url';
+configureZxShellForPlatform();
+
// Calculate paths
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
diff --git a/scripts/patch-electron-builder-nsis.mjs b/scripts/patch-electron-builder-nsis.mjs
new file mode 100644
index 0000000..e4537da
--- /dev/null
+++ b/scripts/patch-electron-builder-nsis.mjs
@@ -0,0 +1,75 @@
+#!/usr/bin/env node
+
+import fs from 'node:fs';
+import path from 'node:path';
+import { createRequire } from 'node:module';
+
+const require = createRequire(import.meta.url);
+const targetFile = require.resolve('app-builder-lib/out/targets/nsis/NsisTarget.js');
+const patchMarker = 'zn-ai patch: retry NSIS stub execution on Windows';
+
+const helperSnippet = `
+// ${patchMarker}
+function isRetryableWindowsSpawnError(error) {
+ const code = error != null && typeof error.code === "string" ? error.code : "";
+ const message = error != null && typeof error.message === "string" ? error.message : "";
+ return (code === "EPERM" ||
+ code === "EACCES" ||
+ message.includes("spawn EPERM") ||
+ message.includes("spawn EACCES") ||
+ message.includes("used by another process"));
+}
+async function execWineWithRetry(file, file64 = null, appArgs = [], options = {}) {
+ const maxAttempts = process.platform === "win32" ? 12 : 1;
+ let attempt = 0;
+ while (true) {
+ attempt++;
+ try {
+ return await (0, wine_1.execWine)(file, file64, appArgs, options);
+ }
+ catch (error) {
+ const shouldRetry = process.platform === "win32" &&
+ attempt < maxAttempts &&
+ isRetryableWindowsSpawnError(error);
+ if (!shouldRetry) {
+ throw error;
+ }
+ builder_util_1.log.warn({
+ attempt,
+ maxAttempts,
+ file,
+ reason: error.message || String(error),
+ }, "retrying NSIS stub execution after transient Windows spawn failure");
+ await new Promise(resolve => setTimeout(resolve, attempt * 1000));
+ }
+ }
+}
+`;
+
+const originalCall = 'await (0, wine_1.execWine)(installerPath, null, [], { env: { __COMPAT_LAYER: "RunAsInvoker" } });';
+const patchedCall = 'await execWineWithRetry(installerPath, null, [], { env: { __COMPAT_LAYER: "RunAsInvoker" } });';
+
+const source = fs.readFileSync(targetFile, 'utf8');
+
+if (source.includes(patchMarker)) {
+ console.log(`[patch-electron-builder-nsis] already patched: ${targetFile}`);
+ process.exit(0);
+}
+
+if (!source.includes(originalCall)) {
+ console.error(`[patch-electron-builder-nsis] target call not found in ${targetFile}`);
+ process.exit(1);
+}
+
+const classToken = 'class NsisTarget extends core_1.Target {';
+if (!source.includes(classToken)) {
+ console.error(`[patch-electron-builder-nsis] class token not found in ${targetFile}`);
+ process.exit(1);
+}
+
+const patchedSource = source
+ .replace(classToken, `${helperSnippet}\n${classToken}`)
+ .replace(originalCall, patchedCall);
+
+fs.writeFileSync(targetFile, patchedSource, 'utf8');
+console.log(`[patch-electron-builder-nsis] patched ${targetFile}`);
diff --git a/scripts/prepare-preinstalled-skills-dev.mjs b/scripts/prepare-preinstalled-skills-dev.mjs
index 017c88d..f14361d 100644
--- a/scripts/prepare-preinstalled-skills-dev.mjs
+++ b/scripts/prepare-preinstalled-skills-dev.mjs
@@ -1,10 +1,13 @@
#!/usr/bin/env zx
import 'zx/globals';
+import { configureZxShellForPlatform } from './configure-zx-shell.mjs';
import { existsSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
+configureZxShellForPlatform();
+
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, '..');
const lockPath = join(ROOT, 'build', 'preinstalled-skills', '.preinstalled-lock.json');