From 6bb25e2ef21940e57fac4274030c1184e0fef9d0 Mon Sep 17 00:00:00 2001 From: btc69m979y-dotcom Date: Fri, 6 Mar 2026 16:35:07 +0800 Subject: [PATCH] feat: pin OpenClaw dependency to versioned releases with auto-fetch and build cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add `openclaw.version` and `openclaw.repo` config in package.json - Add `ensure-openclaw-version.cjs` to auto-clone or checkout the pinned tag - Add build cache in `build-openclaw-runtime.sh` — skip rebuild when version matches - Enhanced `runtime-build-info.json` with openclawVersion and openclawCommit - Fix Windows: prefer Git Bash over WSL bash (WindowsApps\bash.exe) - Fix Windows: normalize PATH env for bash, use relative script path - Fix Windows: add `shell: true` for npm.cmd in openclaw-runtime-host.cjs - Add `.gitattributes` to enforce LF line endings for .sh files - Update README.md and README_zh.md with OpenClaw dev workflow and version management docs Co-Authored-By: Claude Opus 4.6 --- .gitattributes | 2 + README.md | 78 ++++++++-- README_zh.md | 78 ++++++++-- package.json | 17 ++- scripts/build-openclaw-runtime.sh | 49 ++++++- scripts/ensure-openclaw-version.cjs | 188 +++++++++++++++++++++++++ scripts/openclaw-runtime-host.cjs | 1 + scripts/run-build-openclaw-runtime.cjs | 60 +++++++- 8 files changed, 445 insertions(+), 28 deletions(-) create mode 100644 .gitattributes create mode 100644 scripts/ensure-openclaw-version.cjs diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..b464538 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Force LF line endings for shell scripts (required for bash on all platforms) +*.sh text eol=lf diff --git a/README.md b/README.md index d5fda5f..65479aa 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,37 @@ npm run electron:dev The dev server runs at `http://localhost:5175` by default. +#### Develop with OpenClaw Agent Engine + +LobsterAI can use [OpenClaw](https://github.com/openclaw/openclaw) as its agent engine. +The required OpenClaw version is pinned in `package.json` under `openclaw.version`. + +```bash +# First run: automatically clones and builds OpenClaw (may take several minutes) +npm run electron:dev:openclaw + +# Subsequent runs: skips build if the pinned version hasn't changed +npm run electron:dev:openclaw +``` + +By default, OpenClaw source is cloned/managed at `../openclaw` (relative to this repo). Override with: + +```bash +OPENCLAW_SRC=/path/to/openclaw npm run electron:dev:openclaw +``` + +To force a rebuild even when the version hasn't changed: + +```bash +OPENCLAW_FORCE_BUILD=1 npm run electron:dev:openclaw +``` + +To skip the automatic version checkout (e.g., when developing OpenClaw locally): + +```bash +OPENCLAW_SKIP_ENSURE=1 npm run electron:dev:openclaw +``` + ### Production Build ```bash @@ -104,12 +135,8 @@ npm run dist:linux ``` Desktop packaging (macOS / Windows / Linux) bundles a prebuilt OpenClaw runtime under `Resources/cfmind`. -`npm run dist:mac`, `npm run dist:win`, and `npm run dist:linux` will run platform-specific OpenClaw runtime build steps before packaging. -By default, OpenClaw source path resolves to `../openclaw` (relative to this repo). Override with an environment variable when needed: - -```bash -OPENCLAW_SRC=/path/to/openclaw npm run dist:win -``` +The pinned OpenClaw version (`package.json` → `openclaw.version`) is automatically fetched and built during packaging — no manual setup needed. +The build is cached: if the runtime for the pinned version already exists locally, the build step is skipped automatically. You can also build OpenClaw runtime manually: @@ -123,10 +150,10 @@ npm run openclaw:runtime:win-x64 npm run openclaw:runtime:linux-x64 ``` -To run dev with OpenClaw runtime prepared automatically: +Override OpenClaw source path with an environment variable when needed: ```bash -npm run electron:dev:openclaw +OPENCLAW_SRC=/path/to/openclaw npm run dist:win ``` Windows builds bundle a portable Python runtime under `resources/python-win` (included as installer resource `python-win`), so end users do not need to install Python manually. @@ -380,6 +407,41 @@ Cowork session config includes: Currently English and Chinese are supported. Switch languages in the Settings panel. +## OpenClaw Version Management + +LobsterAI pins its OpenClaw dependency to a specific release version, declared in `package.json`: + +```json +{ + "openclaw": { + "version": "v2026.3.2", + "repo": "https://github.com/openclaw/openclaw.git" + } +} +``` + +### How It Works + +| Step | What happens | When | +|------|-------------|------| +| **Version ensure** | Clones or checks out the pinned tag in `../openclaw` | Before every runtime build | +| **Build cache check** | Compares pinned version with `runtime-build-info.json` | Before every runtime build | +| **Full build** | `pnpm install` → `build` → `ui:build` → pack to asar | Only when version changed | + +### Updating OpenClaw Version + +1. Change `openclaw.version` in `package.json` to the desired release tag +2. Run `npm run electron:dev:openclaw` or `npm run dist:win` — the new version is fetched and built automatically +3. Commit the `package.json` change + +### Environment Variables + +| Variable | Description | Default | +|----------|-------------|---------| +| `OPENCLAW_SRC` | Path to OpenClaw source directory | `../openclaw` | +| `OPENCLAW_FORCE_BUILD` | Set to `1` to force rebuild even if version matches | — | +| `OPENCLAW_SKIP_ENSURE` | Set to `1` to skip automatic version checkout | — | + ## Development Guidelines - TypeScript strict mode, functional components + Hooks diff --git a/README_zh.md b/README_zh.md index b263e95..8c71a35 100644 --- a/README_zh.md +++ b/README_zh.md @@ -69,6 +69,37 @@ npm run electron:dev 开发服务器默认运行在 `http://localhost:5175`。 +#### 带 OpenClaw Agent 引擎开发 + +LobsterAI 可以使用 [OpenClaw](https://github.com/openclaw/openclaw) 作为 Agent 引擎。 +所依赖的 OpenClaw 版本在 `package.json` 的 `openclaw.version` 字段中声明。 + +```bash +# 首次运行:自动克隆并构建 OpenClaw(可能需要几分钟) +npm run electron:dev:openclaw + +# 后续运行:如果锁定版本未变,自动跳过构建 +npm run electron:dev:openclaw +``` + +默认 OpenClaw 源码会被克隆/管理在 `../openclaw`(相对于本仓库)。可通过环境变量覆盖: + +```bash +OPENCLAW_SRC=/path/to/openclaw npm run electron:dev:openclaw +``` + +强制重新构建(即使版本未变): + +```bash +OPENCLAW_FORCE_BUILD=1 npm run electron:dev:openclaw +``` + +跳过自动版本切换(如需本地开发 OpenClaw 时): + +```bash +OPENCLAW_SKIP_ENSURE=1 npm run electron:dev:openclaw +``` + ### 生产构建 ```bash @@ -104,12 +135,8 @@ npm run dist:linux ``` 桌面端打包(macOS / Windows / Linux)都会把预构建的 OpenClaw runtime 内置到 `Resources/cfmind`。 -`npm run dist:mac`、`npm run dist:win`、`npm run dist:linux` 在打包前都会自动执行对应平台的 OpenClaw runtime 构建步骤。 -默认 OpenClaw 源码路径为相对本仓库的 `../openclaw`,也可按需覆盖: - -```bash -OPENCLAW_SRC=/path/to/openclaw npm run dist:win -``` +锁定的 OpenClaw 版本(`package.json` → `openclaw.version`)在打包时会自动拉取并构建,无需手动操作。 +构建结果带缓存:如果本地已存在对应版本的 runtime,构建步骤会自动跳过。 也可以手动构建 OpenClaw runtime: @@ -123,10 +150,10 @@ npm run openclaw:runtime:win-x64 npm run openclaw:runtime:linux-x64 ``` -如果希望开发启动前自动准备 OpenClaw runtime: +如需覆盖 OpenClaw 源码路径: ```bash -npm run electron:dev:openclaw +OPENCLAW_SRC=/path/to/openclaw npm run dist:win ``` Windows 打包会内置便携 Python 运行时到 `resources/python-win`(安装包资源目录为 `python-win`),终端用户无需手动安装 Python。 @@ -380,6 +407,41 @@ Cowork 会话配置包含: 支持中文(默认)和英文两种语言,通过设置面板切换。 +## OpenClaw 版本管理 + +LobsterAI 将 OpenClaw 依赖锁定到指定的 release 版本,在 `package.json` 中声明: + +```json +{ + "openclaw": { + "version": "v2026.3.2", + "repo": "https://github.com/openclaw/openclaw.git" + } +} +``` + +### 工作原理 + +| 步骤 | 行为 | 时机 | +|------|------|------| +| **版本确认** | 克隆或切换 `../openclaw` 到锁定的 tag | 每次 runtime 构建前 | +| **构建缓存检查** | 比对锁定版本与 `runtime-build-info.json` | 每次 runtime 构建前 | +| **完整构建** | `pnpm install` → `build` → `ui:build` → 打包为 asar | 仅版本变更时 | + +### 更新 OpenClaw 版本 + +1. 修改 `package.json` 中 `openclaw.version` 为目标 release tag +2. 执行 `npm run electron:dev:openclaw` 或 `npm run dist:win` — 新版本会自动拉取并构建 +3. 提交 `package.json` 的变更 + +### 环境变量 + +| 变量 | 说明 | 默认值 | +|------|------|--------| +| `OPENCLAW_SRC` | OpenClaw 源码目录路径 | `../openclaw` | +| `OPENCLAW_FORCE_BUILD` | 设为 `1` 强制重新构建(即使版本匹配) | — | +| `OPENCLAW_SKIP_ENSURE` | 设为 `1` 跳过自动版本切换 | — | + ## 开发规范 - TypeScript 严格模式,函数式组件 + Hooks diff --git a/package.json b/package.json index 6de126b..ac591aa 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,10 @@ }, "license": "MIT", "version": "0.2.0", + "openclaw": { + "version": "v2026.3.2", + "repo": "https://github.com/openclaw/openclaw.git" + }, "main": "dist-electron/main.js", "engines": { "node": ">=24 <25" @@ -44,13 +48,14 @@ "generate:tray-icons": "node scripts/generate-tray-icons.js", "setup:mingit": "node scripts/setup-mingit.js", "setup:python-runtime": "node scripts/setup-python-runtime.js", + "openclaw:ensure": "node scripts/ensure-openclaw-version.cjs", "openclaw:runtime:host": "node scripts/openclaw-runtime-host.cjs", - "openclaw:runtime:mac-arm64": "node scripts/run-build-openclaw-runtime.cjs mac-arm64 && node scripts/sync-openclaw-runtime-current.cjs mac-arm64", - "openclaw:runtime:mac-x64": "node scripts/run-build-openclaw-runtime.cjs mac-x64 && node scripts/sync-openclaw-runtime-current.cjs mac-x64", - "openclaw:runtime:win-x64": "node scripts/run-build-openclaw-runtime.cjs win-x64 && node scripts/sync-openclaw-runtime-current.cjs win-x64", - "openclaw:runtime:win-arm64": "node scripts/run-build-openclaw-runtime.cjs win-arm64 && node scripts/sync-openclaw-runtime-current.cjs win-arm64", - "openclaw:runtime:linux-x64": "node scripts/run-build-openclaw-runtime.cjs linux-x64 && node scripts/sync-openclaw-runtime-current.cjs linux-x64", - "openclaw:runtime:linux-arm64": "node scripts/run-build-openclaw-runtime.cjs linux-arm64 && node scripts/sync-openclaw-runtime-current.cjs linux-arm64", + "openclaw:runtime:mac-arm64": "npm run openclaw:ensure && node scripts/run-build-openclaw-runtime.cjs mac-arm64 && node scripts/sync-openclaw-runtime-current.cjs mac-arm64", + "openclaw:runtime:mac-x64": "npm run openclaw:ensure && node scripts/run-build-openclaw-runtime.cjs mac-x64 && node scripts/sync-openclaw-runtime-current.cjs mac-x64", + "openclaw:runtime:win-x64": "npm run openclaw:ensure && node scripts/run-build-openclaw-runtime.cjs win-x64 && node scripts/sync-openclaw-runtime-current.cjs win-x64", + "openclaw:runtime:win-arm64": "npm run openclaw:ensure && node scripts/run-build-openclaw-runtime.cjs win-arm64 && node scripts/sync-openclaw-runtime-current.cjs win-arm64", + "openclaw:runtime:linux-x64": "npm run openclaw:ensure && node scripts/run-build-openclaw-runtime.cjs linux-x64 && node scripts/sync-openclaw-runtime-current.cjs linux-x64", + "openclaw:runtime:linux-arm64": "npm run openclaw:ensure && node scripts/run-build-openclaw-runtime.cjs linux-arm64 && node scripts/sync-openclaw-runtime-current.cjs linux-arm64", "regenerate:icon": "bash scripts/regenerate-mac-icon.sh", "fix:mac-icon": "bash scripts/fix-mac-icon-display.sh" }, diff --git a/scripts/build-openclaw-runtime.sh b/scripts/build-openclaw-runtime.sh index 9f2f386..39f9798 100755 --- a/scripts/build-openclaw-runtime.sh +++ b/scripts/build-openclaw-runtime.sh @@ -75,6 +75,31 @@ fi node -e 'const [a,b,c]=process.versions.node.split(".").map(Number);const ok=a>22||(a===22&&(b>12||(b===12&&c>=0)));if(!ok){console.error(`Node ${process.versions.node} is too old. Require >= 22.12.0`);process.exit(1)}' +# --------------------------------------------------------------------------- +# Build cache: skip if the runtime was already built for the pinned version. +# --------------------------------------------------------------------------- +DESIRED_VERSION="" +DESIRED_VERSION=$(node -e " + const pkg = require('$ELECTRON_ROOT/package.json'); + if (pkg.openclaw && pkg.openclaw.version) console.log(pkg.openclaw.version); +" 2>/dev/null || true) + +if [[ -n "$DESIRED_VERSION" && "$OPENCLAW_FORCE_BUILD" != "1" ]]; then + BUILD_INFO="$OUT_DIR/runtime-build-info.json" + if [[ -f "$BUILD_INFO" ]]; then + BUILT_VERSION=$(node -e " + const info = require('$BUILD_INFO'); + console.log(info.openclawVersion || ''); + " 2>/dev/null || true) + if [[ "$BUILT_VERSION" == "$DESIRED_VERSION" ]]; then + echo "[openclaw-runtime] Already built for $DESIRED_VERSION (target=$TARGET_ID), skipping." + echo "[openclaw-runtime] Use OPENCLAW_FORCE_BUILD=1 to force rebuild." + exit 0 + fi + fi + echo "[openclaw-runtime] Pinned version: $DESIRED_VERSION (current build: ${BUILT_VERSION:-none})" +fi + echo "[1/7] Building OpenClaw from source: $OPENCLAW_SRC" pushd "$OPENCLAW_SRC" >/dev/null corepack enable >/dev/null 2>&1 || true @@ -111,16 +136,38 @@ cp -R "$PKG_DIR" "$OUT_DIR" # Save build metadata for traceability. # Use `node -` so stdin is treated as script and the following args remain user args. -node - "$OUT_DIR" "$OPENCLAW_SRC" "$TARGET_ID" <<'NODE' +node - "$OUT_DIR" "$OPENCLAW_SRC" "$TARGET_ID" "$ELECTRON_ROOT" <<'NODE' const fs = require('fs'); const path = require('path'); +const { execFileSync } = require('child_process'); const outDir = process.argv[2]; const src = process.argv[3]; const target = process.argv[4]; +const electronRoot = process.argv[5]; + +// Read pinned version from package.json +let openclawVersion = ''; +try { + const pkg = require(path.join(electronRoot, 'package.json')); + openclawVersion = (pkg.openclaw && pkg.openclaw.version) || ''; +} catch {} + +// Read git commit hash from openclaw source +let openclawCommit = ''; +try { + openclawCommit = execFileSync('git', ['rev-parse', 'HEAD'], { + cwd: src, + encoding: 'utf-8', + stdio: ['ignore', 'pipe', 'ignore'], + }).trim(); +} catch {} + const meta = { builtAt: new Date().toISOString(), source: src, target, + openclawVersion, + openclawCommit, }; fs.writeFileSync(path.join(outDir, 'runtime-build-info.json'), JSON.stringify(meta, null, 2) + '\n'); NODE diff --git a/scripts/ensure-openclaw-version.cjs b/scripts/ensure-openclaw-version.cjs new file mode 100644 index 0000000..cea5efb --- /dev/null +++ b/scripts/ensure-openclaw-version.cjs @@ -0,0 +1,188 @@ +'use strict'; + +/** + * Ensure the local OpenClaw source directory is checked out at the version + * declared in package.json ("openclaw.version"). + * + * - If OPENCLAW_SRC does not exist, clones the repo at the pinned tag. + * - If it exists but is on a different version, fetches and checks out the tag. + * - If already on the correct tag, does nothing. + * + * Environment variables: + * OPENCLAW_SRC – Override the OpenClaw source path (default: ../openclaw) + * OPENCLAW_SKIP_ENSURE – Set to "1" to skip this script entirely + */ + +const { execFileSync, spawnSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +const rootDir = path.resolve(__dirname, '..'); + +function log(msg) { + console.log(`[openclaw-ensure] ${msg}`); +} + +function warn(msg) { + console.warn(`[openclaw-ensure] WARNING: ${msg}`); +} + +function die(msg) { + console.error(`[openclaw-ensure] ERROR: ${msg}`); + process.exit(1); +} + +function commandExists(command) { + const checker = process.platform === 'win32' ? 'where' : 'which'; + const result = spawnSync(checker, [command], { stdio: 'ignore' }); + return result.status === 0; +} + +function resolveGitExecutable() { + if (commandExists('git')) return 'git'; + + // Windows fallback: project-bundled mingit + if (process.platform === 'win32') { + const candidates = [ + path.join(rootDir, 'resources', 'mingit', 'cmd', 'git.exe'), + path.join(rootDir, 'resources', 'mingit', 'bin', 'git.exe'), + ]; + for (const candidate of candidates) { + if (fs.existsSync(candidate)) return candidate; + } + } + + return null; +} + +function git(args, opts = {}) { + const result = execFileSync(gitBin, args, { + encoding: 'utf-8', + stdio: opts.stdio || ['ignore', 'pipe', 'pipe'], + cwd: opts.cwd, + }); + return typeof result === 'string' ? result.trim() : ''; +} + +function gitExitCode(args, opts = {}) { + const result = spawnSync(gitBin, args, { + encoding: 'utf-8', + stdio: opts.stdio || ['ignore', 'pipe', 'pipe'], + cwd: opts.cwd, + }); + return result.status; +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +// Skip if explicitly requested +if (process.env.OPENCLAW_SKIP_ENSURE === '1') { + log('Skipped (OPENCLAW_SKIP_ENSURE=1).'); + process.exit(0); +} + +// Read config from package.json +const pkg = require(path.join(rootDir, 'package.json')); +const openclawConfig = pkg.openclaw; +if (!openclawConfig || !openclawConfig.version) { + die('Missing "openclaw.version" in package.json.'); +} + +const desiredVersion = openclawConfig.version; +const repoUrl = openclawConfig.repo || 'https://github.com/openclaw/openclaw.git'; +const openclawSrc = process.env.OPENCLAW_SRC || path.resolve(rootDir, '..', 'openclaw'); + +log(`Desired OpenClaw version: ${desiredVersion}`); +log(`OpenClaw source path: ${openclawSrc}`); + +// Resolve git binary +const gitBin = resolveGitExecutable(); +if (!gitBin) { + die( + 'git is required but not found.' + + (process.platform === 'win32' + ? ' Install Git for Windows or run `npm run setup:mingit` first.' + : '') + ); +} + +// Case 1: source directory does not exist → clone +if (!fs.existsSync(openclawSrc)) { + log(`Cloning ${repoUrl} at tag ${desiredVersion} ...`); + try { + git( + ['clone', '--branch', desiredVersion, '--depth', '1', repoUrl, openclawSrc], + { stdio: 'inherit' } + ); + } catch (e) { + die(`Failed to clone OpenClaw: ${e.message}`); + } + log(`Cloned successfully at ${desiredVersion}.`); + process.exit(0); +} + +// Case 2: source directory exists → check current version +if (!fs.existsSync(path.join(openclawSrc, '.git'))) { + die(`${openclawSrc} exists but is not a git repository.`); +} + +// Check if already on the desired tag +let currentTag = ''; +try { + currentTag = git(['describe', '--tags', '--exact-match', 'HEAD'], { cwd: openclawSrc }); +} catch { + // HEAD is not on an exact tag — that's fine, we'll need to checkout +} + +if (currentTag === desiredVersion) { + log(`Already at ${desiredVersion}, nothing to do.`); + process.exit(0); +} + +log(`Current: ${currentTag || '(not on a tag)'}. Switching to ${desiredVersion} ...`); + +// Fetch tags (unshallow if needed) +try { + const isShallow = fs.existsSync(path.join(openclawSrc, '.git', 'shallow')); + if (isShallow) { + log('Repository is shallow, fetching full history for tags ...'); + git(['fetch', '--unshallow', '--tags', 'origin'], { cwd: openclawSrc, stdio: 'inherit' }); + } else { + git(['fetch', '--tags', 'origin'], { cwd: openclawSrc, stdio: 'inherit' }); + } +} catch (e) { + die(`Failed to fetch tags: ${e.message}`); +} + +// Verify the desired tag exists +const tagCheck = gitExitCode(['rev-parse', '--verify', `refs/tags/${desiredVersion}`], { + cwd: openclawSrc, +}); +if (tagCheck !== 0) { + die(`Tag ${desiredVersion} not found in the OpenClaw repository. Check openclaw.version in package.json.`); +} + +// Discard any local modifications (typically build artifacts from previous builds) +// before checking out the desired tag. Developers working on OpenClaw itself +// should use OPENCLAW_SKIP_ENSURE=1 to prevent this. +const hasLocalChanges = gitExitCode(['diff', '--quiet', 'HEAD'], { cwd: openclawSrc }) !== 0; +if (hasLocalChanges) { + warn('Discarding local modifications in OpenClaw source (build artifacts).'); + warn('Use OPENCLAW_SKIP_ENSURE=1 if you are developing OpenClaw locally.'); + git(['checkout', '.'], { cwd: openclawSrc }); +} + +// Checkout the desired tag (force to handle any remaining conflicts) +try { + git(['checkout', desiredVersion], { cwd: openclawSrc, stdio: 'inherit' }); +} catch (e) { + die(`Failed to checkout ${desiredVersion}: ${e.message}`); +} + +log(`Switched to ${desiredVersion} successfully.`); diff --git a/scripts/openclaw-runtime-host.cjs b/scripts/openclaw-runtime-host.cjs index 7e2f8ba..f13f673 100644 --- a/scripts/openclaw-runtime-host.cjs +++ b/scripts/openclaw-runtime-host.cjs @@ -32,6 +32,7 @@ const result = spawnSync(npmBin, ['run', `openclaw:runtime:${targetId}`], { cwd: rootDir, env: process.env, stdio: 'inherit', + shell: true, }); if (typeof result.status === 'number') { diff --git a/scripts/run-build-openclaw-runtime.cjs b/scripts/run-build-openclaw-runtime.cjs index 5fb509f..e01d3c5 100644 --- a/scripts/run-build-openclaw-runtime.cjs +++ b/scripts/run-build-openclaw-runtime.cjs @@ -15,10 +15,43 @@ function resolveBashExecutable(rootDir) { return commandExists('bash') ? 'bash' : null; } - if (commandExists('bash')) { - return 'bash'; - } + // On Windows, we must use Git Bash (MSYS2), NOT WSL's bash. + // WSL bash (WindowsApps\bash.exe) runs in a separate Linux environment and + // cannot access Windows-installed node, npm, pnpm, etc. + // 1. Check all bash locations, prefer Git Bash over WSL bash. + try { + const result = spawnSync('where', ['bash'], { + encoding: 'utf-8', + stdio: ['ignore', 'pipe', 'ignore'], + }); + if (result.status === 0 && result.stdout) { + const paths = result.stdout.trim().split(/\r?\n/).map(p => p.trim()).filter(Boolean); + const gitBash = paths.find(p => !p.toLowerCase().includes('windowsapps')); + if (gitBash) return gitBash; + } + } catch {} + + // 2. Derive bash path from git installation. + try { + const gitResult = spawnSync('where', ['git'], { + encoding: 'utf-8', + stdio: ['ignore', 'pipe', 'ignore'], + }); + if (gitResult.status === 0 && gitResult.stdout) { + const gitPath = gitResult.stdout.trim().split(/\r?\n/)[0].trim(); + const gitRoot = path.resolve(path.dirname(gitPath), '..'); + const gitBashCandidates = [ + path.join(gitRoot, 'bin', 'bash.exe'), + path.join(gitRoot, 'usr', 'bin', 'bash.exe'), + ]; + for (const candidate of gitBashCandidates) { + if (fs.existsSync(candidate)) return candidate; + } + } + } catch {} + + // 3. Bundled mingit bash. const candidates = [ path.join(rootDir, 'resources', 'mingit', 'bin', 'bash.exe'), path.join(rootDir, 'resources', 'mingit', 'usr', 'bin', 'bash.exe'), @@ -48,10 +81,27 @@ if (!bashExecutable) { process.exit(1); } -const scriptPath = path.join(rootDir, 'scripts', 'build-openclaw-runtime.sh'); +// On Windows, normalise the environment for bash: +// 1. Bash expects "PATH" (uppercase) but Windows may use "Path" — merge all +// case-variants into a single uppercase "PATH". +// 2. Prepend the directory containing the current node binary so that node, +// npm, pnpm, etc. are findable inside the bash script even when spawned +// through deeply-nested npm/cmd.exe process chains. +const env = { ...process.env }; +if (process.platform === 'win32') { + const nodeDir = path.dirname(process.execPath); + const pathEntries = Object.entries(env).filter(([k]) => k.toUpperCase() === 'PATH'); + const pathValue = pathEntries.map(([, v]) => v).join(path.delimiter); + for (const [k] of pathEntries) delete env[k]; + env.PATH = `${nodeDir}${path.delimiter}${pathValue}`; +} + +// Use a relative path so bash never sees Windows drive-letter paths like +// "D:/..." which can fail when invoked through nested npm/cmd.exe chains. +const scriptPath = 'scripts/build-openclaw-runtime.sh'; const result = spawnSync(bashExecutable, [scriptPath, targetId], { cwd: rootDir, - env: process.env, + env, stdio: 'inherit', });