feat(openfang): add bundled OpenFang runtime support

- Add prepare-openfang-runtime.mjs script for cross-platform binary download
- Update lib.rs to support binary runtime (fallback to Node.js for legacy)
- Add openfang.cmd/sh launcher scripts
- Update runtime-manifest.json for binary-based runtime
- Add README documentation for bundled runtime architecture

OpenFang binary is downloaded during build, supporting:
- Windows x64/ARM64 (.zip)
- macOS Intel/Apple Silicon (.tar.gz)
- Linux x64/ARM64 (.tar.gz)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
iven
2026-03-13 18:03:43 +08:00
parent cfb06d7209
commit 4eb164764a
10 changed files with 1384 additions and 12 deletions

View File

@@ -0,0 +1,423 @@
#!/usr/bin/env node
/**
* OpenFang Runtime Preparation Script
*
* Prepares the OpenFang binary for bundling with Tauri.
* Supports cross-platform: Windows, Linux, macOS
*
* Usage:
* node scripts/prepare-openfang-runtime.mjs
* node scripts/prepare-openfang-runtime.mjs --dry-run
* OPENFANG_VERSION=v1.2.3 node scripts/prepare-openfang-runtime.mjs
*/
import { execSync, execFileSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { arch as osArch, platform as osPlatform, homedir } from 'node:os';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const desktopRoot = path.resolve(__dirname, '..');
const outputDir = path.join(desktopRoot, 'src-tauri', 'resources', 'openfang-runtime');
const dryRun = process.argv.includes('--dry-run');
const openfangVersion = process.env.OPENFANG_VERSION || 'latest';
const PLATFORM = osPlatform();
const ARCH = osArch();
function log(message) {
console.log(`[prepare-openfang-runtime] ${message}`);
}
function warn(message) {
console.warn(`[prepare-openfang-runtime] WARN: ${message}`);
}
function error(message) {
console.error(`[prepare-openfang-runtime] ERROR: ${message}`);
}
/**
* Get platform-specific binary configuration
* OpenFang releases: .zip for Windows, .tar.gz for Unix
*/
function getPlatformConfig() {
const configs = {
win32: {
x64: {
binaryName: 'openfang.exe',
downloadName: 'openfang-x86_64-pc-windows-msvc.zip',
archiveFormat: 'zip',
},
arm64: {
binaryName: 'openfang.exe',
downloadName: 'openfang-aarch64-pc-windows-msvc.zip',
archiveFormat: 'zip',
},
},
darwin: {
x64: {
binaryName: 'openfang-x86_64-apple-darwin',
downloadName: 'openfang-x86_64-apple-darwin.tar.gz',
archiveFormat: 'tar.gz',
},
arm64: {
binaryName: 'openfang-aarch64-apple-darwin',
downloadName: 'openfang-aarch64-apple-darwin.tar.gz',
archiveFormat: 'tar.gz',
},
},
linux: {
x64: {
binaryName: 'openfang-x86_64-unknown-linux-gnu',
downloadName: 'openfang-x86_64-unknown-linux-gnu.tar.gz',
archiveFormat: 'tar.gz',
},
arm64: {
binaryName: 'openfang-aarch64-unknown-linux-gnu',
downloadName: 'openfang-aarch64-unknown-linux-gnu.tar.gz',
archiveFormat: 'tar.gz',
},
},
};
const platformConfig = configs[PLATFORM];
if (!platformConfig) {
throw new Error(`Unsupported platform: ${PLATFORM}`);
}
const archConfig = platformConfig[ARCH];
if (!archConfig) {
throw new Error(`Unsupported architecture: ${ARCH} on ${PLATFORM}`);
}
return archConfig;
}
/**
* Find OpenFang binary in system PATH
*/
function findSystemBinary() {
const override = process.env.OPENFANG_BIN;
if (override) {
if (fs.existsSync(override)) {
return override;
}
throw new Error(`OPENFANG_BIN specified but file not found: ${override}`);
}
try {
let result;
if (PLATFORM === 'win32') {
result = execFileSync('where.exe', ['openfang'], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
});
} else {
result = execFileSync('which', ['openfang'], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
});
}
const binaryPath = result.split(/\r?\n/).map(s => s.trim()).find(Boolean);
if (binaryPath && fs.existsSync(binaryPath)) {
return binaryPath;
}
} catch {
// Binary not found in PATH
}
return null;
}
/**
* Check if OpenFang is installed via install script
*/
function findInstalledBinary() {
const config = getPlatformConfig();
const home = homedir();
const possiblePaths = [
// Default install location
path.join(home, '.openfang', 'bin', config.binaryName),
path.join(home, '.local', 'bin', config.binaryName),
// macOS
path.join(home, '.openfang', 'bin', 'openfang'),
'/usr/local/bin/openfang',
'/usr/bin/openfang',
];
for (const p of possiblePaths) {
if (fs.existsSync(p)) {
return p;
}
}
return null;
}
/**
* Download OpenFang binary from GitHub Releases
* Handles .zip for Windows, .tar.gz for Unix
*/
function downloadBinary(config) {
const baseUrl = 'https://github.com/RightNow-AI/openfang/releases';
const downloadUrl = openfangVersion === 'latest'
? `${baseUrl}/latest/download/${config.downloadName}`
: `${baseUrl}/download/${openfangVersion}/${config.downloadName}`;
const archivePath = path.join(outputDir, config.downloadName);
const binaryOutputPath = path.join(outputDir, config.binaryName);
log(`Downloading OpenFang binary...`);
log(` Platform: ${PLATFORM} (${ARCH})`);
log(` Version: ${openfangVersion}`);
log(` Archive: ${config.downloadName}`);
log(` URL: ${downloadUrl}`);
if (dryRun) {
log('DRY RUN: Would download and extract binary');
return null;
}
// Ensure output directory exists
fs.mkdirSync(outputDir, { recursive: true });
try {
// Download archive using curl (works on all platforms)
log('Downloading archive...');
execSync(`curl -fsSL -o "${archivePath}" "${downloadUrl}"`, { stdio: 'inherit' });
if (!fs.existsSync(archivePath)) {
throw new Error('Download failed - archive not created');
}
// Extract archive
log('Extracting binary...');
if (config.archiveFormat === 'zip') {
// Use PowerShell to extract zip on Windows
execSync(
`powershell -Command "Expand-Archive -Path '${archivePath}' -DestinationPath '${outputDir}' -Force"`,
{ stdio: 'inherit' }
);
} else {
// Use tar for .tar.gz on Unix
execSync(`tar -xzf "${archivePath}" -C "${outputDir}"`, { stdio: 'inherit' });
}
// Find and rename the extracted binary
// The archive contains a single binary file
const extractedFiles = fs.readdirSync(outputDir).filter(f =>
f.startsWith('openfang') && !f.endsWith('.zip') && !f.endsWith('.tar.gz') && !f.endsWith('.sha256')
);
if (extractedFiles.length === 0) {
throw new Error('No binary found in archive');
}
// Use the first extracted binary (should be only one)
const extractedBinary = path.join(outputDir, extractedFiles[0]);
log(`Found extracted binary: ${extractedFiles[0]}`);
// Rename to standard name if needed
if (extractedFiles[0] !== config.binaryName) {
if (fs.existsSync(binaryOutputPath)) {
fs.unlinkSync(binaryOutputPath);
}
fs.renameSync(extractedBinary, binaryOutputPath);
log(`Renamed to: ${config.binaryName}`);
}
// Make executable on Unix
if (PLATFORM !== 'win32') {
fs.chmodSync(binaryOutputPath, 0o755);
}
// Clean up archive
fs.unlinkSync(archivePath);
log('Cleaned up archive file');
log(`Binary ready at: ${binaryOutputPath}`);
return binaryOutputPath;
} catch (err) {
error(`Failed to download/extract: ${err.message}`);
// Clean up partial files
if (fs.existsSync(archivePath)) {
fs.unlinkSync(archivePath);
}
return null;
}
}
/**
* Copy binary to output directory
*/
function copyBinary(sourcePath, config) {
if (dryRun) {
log(`DRY RUN: Would copy binary from ${sourcePath}`);
return;
}
fs.mkdirSync(outputDir, { recursive: true });
const destPath = path.join(outputDir, config.binaryName);
fs.copyFileSync(sourcePath, destPath);
// Make executable on Unix
if (PLATFORM !== 'win32') {
fs.chmodSync(destPath, 0o755);
}
log(`Copied binary to: ${destPath}`);
}
/**
* Write runtime manifest
*/
function writeManifest(config) {
if (dryRun) {
log('DRY RUN: Would write manifest');
return;
}
const manifest = {
source: {
binPath: config.binaryName,
binPathLinux: 'openfang-x86_64-unknown-linux-gnu',
binPathMac: 'openfang-x86_64-apple-darwin',
binPathMacArm: 'openfang-aarch64-apple-darwin',
},
stagedAt: new Date().toISOString(),
version: openfangVersion === 'latest'
? new Date().toISOString().split('T')[0].replace(/-/g, '.')
: openfangVersion,
runtimeType: 'openfang',
description: 'OpenFang Agent OS - Single binary runtime (~32MB)',
endpoints: {
websocket: 'ws://127.0.0.1:4200/ws',
rest: 'http://127.0.0.1:4200/api',
},
platform: {
os: PLATFORM,
arch: ARCH,
},
};
const manifestPath = path.join(outputDir, 'runtime-manifest.json');
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf8');
log(`Manifest written to: ${manifestPath}`);
}
/**
* Write launcher scripts for convenience
*/
function writeLauncherScripts(config) {
if (dryRun) {
log('DRY RUN: Would write launcher scripts');
return;
}
// Windows launcher
const cmdLauncher = [
'@echo off',
'REM OpenFang Agent OS - Bundled Binary Launcher',
`"%~dp0${config.binaryName}" %*`,
'',
].join('\r\n');
fs.writeFileSync(path.join(outputDir, 'openfang.cmd'), cmdLauncher, 'utf8');
// Unix launcher
const shLauncher = [
'#!/bin/bash',
'# OpenFang Agent OS - Bundled Binary Launcher',
`SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"`,
`exec "$SCRIPT_DIR/${config.binaryName}" "$@"`,
'',
].join('\n');
const shPath = path.join(outputDir, 'openfang.sh');
fs.writeFileSync(shPath, shLauncher, 'utf8');
fs.chmodSync(shPath, 0o755);
log('Launcher scripts written');
}
/**
* Clean old OpenClaw runtime files
*/
function cleanOldRuntime() {
const oldPaths = [
path.join(outputDir, 'node.exe'),
path.join(outputDir, 'node_modules'),
path.join(outputDir, 'openclaw.cmd'),
];
for (const p of oldPaths) {
if (fs.existsSync(p)) {
if (dryRun) {
log(`DRY RUN: Would remove ${p}`);
} else {
fs.rmSync(p, { recursive: true, force: true });
log(`Removed old file: ${p}`);
}
}
}
}
/**
* Main function
*/
function main() {
log('='.repeat(60));
log('OpenFang Runtime Preparation');
log('='.repeat(60));
const config = getPlatformConfig();
log(`Platform: ${PLATFORM} (${ARCH})`);
log(`Binary: ${config.binaryName}`);
log(`Output: ${outputDir}`);
// Clean old OpenClaw runtime
cleanOldRuntime();
// Try to find existing binary
let binaryPath = findSystemBinary();
if (binaryPath) {
log(`Found OpenFang in PATH: ${binaryPath}`);
copyBinary(binaryPath, config);
} else {
binaryPath = findInstalledBinary();
if (binaryPath) {
log(`Found installed OpenFang: ${binaryPath}`);
copyBinary(binaryPath, config);
} else {
log('OpenFang not found locally, downloading...');
const downloaded = downloadBinary(config);
if (!downloaded && !dryRun) {
error('Failed to obtain OpenFang binary!');
error('');
error('Please either:');
error(' 1. Install OpenFang: curl -fsSL https://openfang.sh/install | sh');
error(' 2. Set OPENFANG_BIN environment variable to binary path');
error(' 3. Manually download from: https://github.com/RightNow-AI/openfang/releases');
process.exit(1);
}
}
}
// Write supporting files
writeManifest(config);
writeLauncherScripts(config);
log('='.repeat(60));
if (dryRun) {
log('DRY RUN complete. No files were written.');
} else {
log('OpenFang runtime ready for build!');
}
log('='.repeat(60));
}
main();