mirror of
https://gh-proxy.org/https://github.com/klinsmaya/LobsterAI.git
synced 2026-11-08 01:06:44 +08:00
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:
co-authored by
Claude Opus 4.6
parent
8c8ae6070e
commit
6bb25e2ef2
@@ -0,0 +1,2 @@
|
||||
# Force LF line endings for shell scripts (required for bash on all platforms)
|
||||
*.sh text eol=lf
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.`);
|
||||
@@ -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') {
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user