feat: pin OpenClaw dependency to versioned releases with auto-fetch and build cache

- 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 <[email protected]>
This commit is contained in:
btc69m979y-dotcom
2026-03-06 16:35:07 +08:00
co-authored by Claude Opus 4.6
parent 8c8ae6070e
commit 6bb25e2ef2
8 changed files with 445 additions and 28 deletions
+2
View File
@@ -0,0 +1,2 @@
# Force LF line endings for shell scripts (required for bash on all platforms)
*.sh text eol=lf
+70 -8
View File
@@ -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
+70 -8
View File
@@ -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
+11 -6
View File
@@ -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"
},
+48 -1
View File
@@ -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
+188
View File
@@ -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.`);
+1
View File
@@ -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') {
+55 -5
View File
@@ -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',
});