#!/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();