From 1cb424dceae0e8a4aeeddc6cf9220128bb98bffd Mon Sep 17 00:00:00 2001 From: 1BigBear <181850822+1BigBear@users.noreply.github.com> Date: Sun, 1 Mar 2026 18:25:55 +0300 Subject: [PATCH 01/10] Add GitHub Actions workflow for ARM build --- .github/workflows/build-openfang-full-arm.yml | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .github/workflows/build-openfang-full-arm.yml diff --git a/.github/workflows/build-openfang-full-arm.yml b/.github/workflows/build-openfang-full-arm.yml new file mode 100644 index 000000000..b4923c04c --- /dev/null +++ b/.github/workflows/build-openfang-full-arm.yml @@ -0,0 +1,47 @@ +name: Build OpenFang Full (ARM Ubuntu) + +on: + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-24.04-arm + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: System deps (build) + run: | + sudo apt-get update + sudo apt-get install -y \ + build-essential pkg-config clang cmake perl git curl \ + libssl-dev libsqlite3-dev \ + libgtk-3-dev libayatana-appindicator3-dev libwebkit2gtk-4.1-dev + + - name: Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Build full workspace + env: + CARGO_BUILD_JOBS: "2" + RUSTFLAGS: "-C debuginfo=0" + run: | + cargo build --release --workspace --locked + + - name: Collect release binaries + run: | + mkdir -p out/bin + find target/release -maxdepth 1 -type f -executable \ + ! -name "*.d" ! -name "build-script-*" -exec cp {} out/bin/ \; + ls -lah out/bin + + - name: Package + run: | + tar -C out -czf openfang-full-aarch64-ubuntu24.tar.gz bin + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: openfang-full-aarch64-ubuntu24 + path: openfang-full-aarch64-ubuntu24.tar.gz From 29683ca9d072706c888b9f5550409987a2fa7493 Mon Sep 17 00:00:00 2001 From: 1BigBear <181850822+1BigBear@users.noreply.github.com> Date: Sun, 8 Mar 2026 15:14:53 +0300 Subject: [PATCH 02/10] Add files via upload --- .github/workflows/release-android.yml | 312 ++++++++++++++++++++++++++ 1 file changed, 312 insertions(+) create mode 100644 .github/workflows/release-android.yml diff --git a/.github/workflows/release-android.yml b/.github/workflows/release-android.yml new file mode 100644 index 000000000..c13ce0e45 --- /dev/null +++ b/.github/workflows/release-android.yml @@ -0,0 +1,312 @@ +name: Release + +on: + push: + tags: + - "v*" + +permissions: + contents: write + packages: write + +env: + CARGO_TERM_COLOR: always + +jobs: + # ── Tauri Desktop App (Windows + macOS + Linux) ─────────────────────────── + # Produces: .msi, .exe (Windows) | .dmg, .app (macOS) | .AppImage, .deb (Linux) + # Also generates and uploads latest.json (the auto-updater manifest) + desktop: + name: Desktop / ${{ matrix.platform.name }} + strategy: + fail-fast: false + matrix: + platform: + - name: Linux x86_64 + os: ubuntu-22.04 + args: "--target x86_64-unknown-linux-gnu" + rust_target: x86_64-unknown-linux-gnu + + - name: macOS x86_64 + os: macos-latest + args: "--target x86_64-apple-darwin" + rust_target: x86_64-apple-darwin + + - name: macOS ARM64 + os: macos-latest + args: "--target aarch64-apple-darwin" + rust_target: aarch64-apple-darwin + + - name: Windows x86_64 + os: windows-latest + args: "--target x86_64-pc-windows-msvc" + rust_target: x86_64-pc-windows-msvc + + - name: Windows ARM64 + os: windows-latest + args: "--target aarch64-pc-windows-msvc" + rust_target: aarch64-pc-windows-msvc + + runs-on: ${{ matrix.platform.os }} + steps: + - uses: actions/checkout@v4 + + - name: Install system deps (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y \ + libwebkit2gtk-4.1-dev \ + libgtk-3-dev \ + libayatana-appindicator3-dev \ + librsvg2-dev \ + patchelf + + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.platform.rust_target }} + + - uses: Swatinem/rust-cache@v2 + with: + key: desktop-${{ matrix.platform.rust_target }} + + - name: Import macOS signing certificate + if: runner.os == 'macOS' + env: + MAC_CERT_BASE64: ${{ secrets.MAC_CERT_BASE64 }} + MAC_CERT_PASSWORD: ${{ secrets.MAC_CERT_PASSWORD }} + run: | + echo "$MAC_CERT_BASE64" | base64 --decode > $RUNNER_TEMP/certificate.p12 + KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db + KEYCHAIN_PASSWORD=$(openssl rand -base64 32) + security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" + security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + security import $RUNNER_TEMP/certificate.p12 -P "$MAC_CERT_PASSWORD" \ + -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH" + security list-keychain -d user -s "$KEYCHAIN_PATH" + security set-key-partition-list -S apple-tool:,apple:,codesign: \ + -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + IDENTITY=$(security find-identity -v -p codesigning "$KEYCHAIN_PATH" | grep "Developer ID Application" | head -1 | awk -F'"' '{print $2}') + echo "Using signing identity: $IDENTITY" + echo "APPLE_SIGNING_IDENTITY=$IDENTITY" >> $GITHUB_ENV + rm -f $RUNNER_TEMP/certificate.p12 + + - name: Build and bundle Tauri desktop app + uses: tauri-apps/tauri-action@v0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + APPLE_SIGNING_IDENTITY: ${{ env.APPLE_SIGNING_IDENTITY }} + APPLE_ID: ${{ secrets.MAC_NOTARIZE_APPLE_ID }} + APPLE_PASSWORD: ${{ secrets.MAC_NOTARIZE_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.MAC_NOTARIZE_TEAM_ID }} + with: + tagName: ${{ github.ref_name }} + releaseName: "OpenFang ${{ github.ref_name }}" + releaseBody: | + ## What's New + + See the [CHANGELOG](https://github.com/RightNow-AI/openfang/blob/main/CHANGELOG.md) for full details. + + ## Installation + + **Desktop App** — Download the installer for your platform below. + + **CLI (Linux/macOS)**: + ```bash + curl -sSf https://openfang.sh | sh + ``` + + **Docker**: + ```bash + docker pull ghcr.io/rightnow-ai/openfang:latest + ``` + + **Coming from OpenClaw?** + ```bash + openfang migrate --from openclaw + ``` + releaseDraft: false + prerelease: false + includeUpdaterJson: true + projectPath: crates/openfang-desktop + args: ${{ matrix.platform.args }} + + # ── CLI Binary (7 platforms + native ARM64 for Android/proot) ──────────── + cli: + name: CLI / ${{ matrix.target }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - target: x86_64-unknown-linux-gnu + os: ubuntu-22.04 + archive: tar.gz + native: false + + - target: aarch64-unknown-linux-gnu + os: ubuntu-22.04 + archive: tar.gz + native: false + + # Native ARM64 build on Ubuntu 24 — links against OpenSSL 3.x + # Works in proot-distro Ubuntu 24 and nix-on-droid without OpenSSL errors + - target: aarch64-unknown-linux-gnu + os: ubuntu-24.04-arm + archive: tar.gz + native: true + artifact_name: aarch64-unknown-linux-gnu-native + + # Static musl build — works on Android nix-on-droid without any libc + - target: aarch64-unknown-linux-musl + os: ubuntu-22.04 + archive: tar.gz + native: false + + - target: x86_64-apple-darwin + os: macos-latest + archive: tar.gz + native: false + + - target: aarch64-apple-darwin + os: macos-latest + archive: tar.gz + native: false + + - target: x86_64-pc-windows-msvc + os: windows-latest + archive: zip + native: false + + - target: aarch64-pc-windows-msvc + os: windows-latest + archive: zip + native: false + + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - name: Install build deps (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y \ + build-essential pkg-config clang cmake perl curl \ + libssl-dev libsqlite3-dev + + - name: Install musl cross-compiler (aarch64-musl) + if: matrix.target == 'aarch64-unknown-linux-musl' + run: sudo apt-get install -y musl-tools gcc-aarch64-linux-gnu + + - name: Install cross (non-native aarch64) + if: contains(fromJSON('["aarch64-unknown-linux-gnu","aarch64-unknown-linux-musl"]'), matrix.target) && matrix.native == false + run: cargo install cross --locked + + - uses: Swatinem/rust-cache@v2 + with: + key: cli-${{ matrix.target }}-${{ matrix.os }} + + # Native ARM64 build — same approach as build-openfang-full-arm workflow + - name: Build CLI (native ARM64) + if: matrix.native == true + env: + CARGO_BUILD_JOBS: "2" + RUSTFLAGS: "-C debuginfo=0" + run: cargo build --release --locked --bin openfang + + # Cross-compiled aarch64 builds + - name: Build CLI (cross) + if: contains(fromJSON('["aarch64-unknown-linux-gnu","aarch64-unknown-linux-musl"]'), matrix.target) && matrix.native == false + env: + OPENSSL_STATIC: "1" + OPENSSL_VENDORED: "1" + run: cross build --release --target ${{ matrix.target }} --bin openfang + + # All other platforms + - name: Build CLI + if: matrix.native == false && matrix.target != 'aarch64-unknown-linux-gnu' && matrix.target != 'aarch64-unknown-linux-musl' + run: cargo build --release --target ${{ matrix.target }} --bin openfang + + - name: Ad-hoc codesign CLI binary (macOS) + if: runner.os == 'macOS' + run: codesign --force --sign - target/${{ matrix.target }}/release/openfang + + - name: Set artifact name + id: artifact + run: | + if [ -n "${{ matrix.artifact_name }}" ]; then + echo "name=${{ matrix.artifact_name }}" >> $GITHUB_OUTPUT + else + echo "name=${{ matrix.target }}" >> $GITHUB_OUTPUT + fi + shell: bash + + - name: Set binary path + id: binpath + run: | + if [ "${{ matrix.native }}" = "true" ]; then + echo "path=target/release/openfang" >> $GITHUB_OUTPUT + else + echo "path=target/${{ matrix.target }}/release/openfang" >> $GITHUB_OUTPUT + fi + shell: bash + + - name: Package (Unix) + if: matrix.archive == 'tar.gz' + run: | + cp ${{ steps.binpath.outputs.path }} openfang + tar czf openfang-${{ steps.artifact.outputs.name }}.tar.gz openfang + sha256sum openfang-${{ steps.artifact.outputs.name }}.tar.gz > openfang-${{ steps.artifact.outputs.name }}.tar.gz.sha256 + + - name: Package (Windows) + if: matrix.archive == 'zip' + shell: pwsh + run: | + Compress-Archive -Path "target/${{ matrix.target }}/release/openfang.exe" -DestinationPath "openfang-${{ matrix.target }}.zip" + $hash = (Get-FileHash "openfang-${{ matrix.target }}.zip" -Algorithm SHA256).Hash.ToLower() + "$hash openfang-${{ matrix.target }}.zip" | Out-File -Encoding ASCII "openfang-${{ matrix.target }}.zip.sha256" + + - name: Upload to GitHub Release + uses: softprops/action-gh-release@v2 + with: + files: openfang-${{ steps.artifact.outputs.name }}.* + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # ── Docker (linux/amd64 + linux/arm64) ──────────────────────────────────── + docker: + name: Docker Image + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Set up QEMU (for arm64 emulation) + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Extract version + id: version + run: echo "version=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_OUTPUT" + - name: Build and push (multi-arch) + uses: docker/build-push-action@v6 + with: + context: . + push: true + platforms: linux/amd64,linux/arm64 + tags: | + ghcr.io/rightnow-ai/openfang:latest + ghcr.io/rightnow-ai/openfang:${{ steps.version.outputs.version }} + cache-from: type=gha + cache-to: type=gha,mode=max From f119a668e1f23db9878c62764aa8b03ba4972a94 Mon Sep 17 00:00:00 2001 From: 1BigBear <181850822+1BigBear@users.noreply.github.com> Date: Sun, 8 Mar 2026 15:16:24 +0300 Subject: [PATCH 03/10] Add files via upload --- install.sh | 177 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 install.sh diff --git a/install.sh b/install.sh new file mode 100644 index 000000000..8618a03ee --- /dev/null +++ b/install.sh @@ -0,0 +1,177 @@ +#!/bin/sh +set -eu + +# OpenFang Installer (1BigBear fork - with musl/Android/proot support) +# https://raw.githubusercontent.com/1BigBear/openfang/main/install.sh + +REPO="1BigBear/openfang" +INSTALL_DIR="$HOME/.openfang" +BIN_DIR="$HOME/.openfang/bin" +BINARY="openfang" + +main() { + need_cmd curl + need_cmd tar + need_cmd uname + + _os="$(uname -s)" + _arch="$(uname -m)" + + # Detect environment on Linux aarch64 + _libc="gnu" + _native="false" + if [ "$_os" = "Linux" ]; then + # Check for musl (nix-on-droid) + if ldd /bin/sh 2>&1 | grep -q musl; then + _libc="musl" + # Check for proot-distro Ubuntu 24 — use native build with OpenSSL 3.x + elif [ -f /etc/os-release ]; then + _ubuntu_ver="$(grep '^VERSION_ID' /etc/os-release 2>/dev/null | cut -d'"' -f2)" + if [ "$_ubuntu_ver" = "24.04" ] || [ "$_ubuntu_ver" = "24.10" ]; then + _native="true" + fi + fi + fi + + case "$_os" in + Linux) + case "$_arch" in + x86_64|amd64) + _target="x86_64-unknown-linux-gnu" + ;; + aarch64|arm64) + if [ "$_libc" = "musl" ]; then + # nix-on-droid — static musl binary, no libc dependency + _target="aarch64-unknown-linux-musl" + elif [ "$_native" = "true" ]; then + # proot-distro Ubuntu 24 — native build with OpenSSL 3.x + _target="aarch64-unknown-linux-gnu-native" + else + # fallback gnu build + _target="aarch64-unknown-linux-gnu" + fi + ;; + *) + err "Unsupported architecture: $_arch" + ;; + esac + ;; + Darwin) + case "$_arch" in + x86_64|amd64) _target="x86_64-apple-darwin" ;; + aarch64|arm64) _target="aarch64-apple-darwin" ;; + *) err "Unsupported architecture: $_arch" ;; + esac + ;; + *) + err "Unsupported OS: $_os (use irm https://openfang.sh/install.ps1 | iex on Windows)" + ;; + esac + + _url="https://github.com/${REPO}/releases/latest/download/openfang-${_target}.tar.gz" + + say "Detected: $_os $_arch -> $_target" + say "Downloading from: $_url" + + _tmpdir="$(mktemp -d 2>/dev/null || mktemp -d -t openfang)" + trap 'rm -rf "$_tmpdir"' EXIT + + _code=$(curl -fsSL -w "%{http_code}" "$_url" -o "${_tmpdir}/openfang.tar.gz") || true + if [ "$_code" = "404" ]; then + err "Release not found for ${_target}. Check https://github.com/${REPO}/releases" + fi + if [ ! -f "${_tmpdir}/openfang.tar.gz" ] || [ "$(wc -c < "${_tmpdir}/openfang.tar.gz")" -lt 1000 ]; then + err "Download failed (HTTP ${_code}). Check https://github.com/${REPO}/releases" + fi + + say "Extracting..." + tar -xzf "${_tmpdir}/openfang.tar.gz" -C "$_tmpdir" + + # Find the binary + _bin="$(find "$_tmpdir" -name "$BINARY" -type f -perm +111 2>/dev/null | head -1)" + if [ -z "$_bin" ]; then + _bin="$(find "$_tmpdir" -name "$BINARY" -type f | head -1)" + fi + if [ -z "$_bin" ]; then + err "Could not find openfang binary in archive" + fi + + mkdir -p "$BIN_DIR" + cp "$_bin" "${BIN_DIR}/${BINARY}" + chmod +x "${BIN_DIR}/${BINARY}" + + # Verify it runs + if "${BIN_DIR}/${BINARY}" --version >/dev/null 2>&1; then + _ver=$("${BIN_DIR}/${BINARY}" --version 2>/dev/null || echo "unknown") + say "Installed: $_ver" + else + say "Installed to: ${BIN_DIR}/${BINARY}" + fi + + add_to_path + + say "" + say "OpenFang installed successfully!" + say "" + say " Run: openfang init" + say " Docs: https://openfang.sh/docs" + say "" + + # Check if binary is reachable + if ! command -v openfang >/dev/null 2>&1; then + say "Note: restart your shell or run:" + say " export PATH=\"${BIN_DIR}:\$PATH\"" + say "" + fi +} + +add_to_path() { + # Skip if already in PATH + case ":$PATH:" in + *":${BIN_DIR}:"*) return ;; + esac + + _line="export PATH=\"${BIN_DIR}:\$PATH\"" + + # Detect shell profile + _profile="" + if [ -n "${SHELL:-}" ]; then + case "$SHELL" in + */zsh) _profile="$HOME/.zshrc" ;; + */bash) + if [ -f "$HOME/.bashrc" ]; then + _profile="$HOME/.bashrc" + else + _profile="$HOME/.bash_profile" + fi ;; + */fish) _profile="$HOME/.config/fish/config.fish" ;; + *) _profile="$HOME/.profile" ;; + esac + else + _profile="$HOME/.profile" + fi + + if [ -n "$_profile" ] && [ -f "$_profile" ]; then + if ! grep -q "/.openfang/bin" "$_profile" 2>/dev/null; then + printf "\n# OpenFang\n%s\n" "$_line" >> "$_profile" + say "Added to PATH via $_profile" + fi + fi +} + +say() { + printf " \033[1;36mopenfang\033[0m %s\n" "$1" +} + +err() { + printf " \033[1;36mopenfang\033[0m \033[1;31merror:\033[0m %s\n" "$1" >&2 + exit 1 +} + +need_cmd() { + if ! command -v "$1" >/dev/null 2>&1; then + err "need '$1' (command not found)" + fi +} + +main From dd16ea15bd8101a96d51abb4ae7d3264e64c54ff Mon Sep 17 00:00:00 2001 From: 1BigBear <181850822+1BigBear@users.noreply.github.com> Date: Sun, 8 Mar 2026 15:20:04 +0300 Subject: [PATCH 04/10] Add workflow_dispatch trigger to release-android.yml --- .github/workflows/release-android.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release-android.yml b/.github/workflows/release-android.yml index c13ce0e45..7de697d12 100644 --- a/.github/workflows/release-android.yml +++ b/.github/workflows/release-android.yml @@ -4,6 +4,7 @@ on: push: tags: - "v*" + workflow_dispatch: permissions: contents: write From 9c76893fdbb346494198b69698228f80514b0e35 Mon Sep 17 00:00:00 2001 From: 1BigBear <181850822+1BigBear@users.noreply.github.com> Date: Sun, 8 Mar 2026 16:26:44 +0300 Subject: [PATCH 05/10] Change project title to 'Self experimenting on OpenFang' Updated project title to reflect self-experimentation. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 746b84277..cbe0819b0 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ OpenFang Logo

-

OpenFang

+

Self experimenting on OpenFang

The Agent Operating System

From 4cfed23a28ec7c28fdf161ce02c459b8d6ee8cc7 Mon Sep 17 00:00:00 2001 From: 1BigBear <181850822+1BigBear@users.noreply.github.com> Date: Sun, 8 Mar 2026 16:43:31 +0300 Subject: [PATCH 06/10] Delete .github/workflows/release-android.yml --- .github/workflows/release-android.yml | 313 -------------------------- 1 file changed, 313 deletions(-) delete mode 100644 .github/workflows/release-android.yml diff --git a/.github/workflows/release-android.yml b/.github/workflows/release-android.yml deleted file mode 100644 index 7de697d12..000000000 --- a/.github/workflows/release-android.yml +++ /dev/null @@ -1,313 +0,0 @@ -name: Release - -on: - push: - tags: - - "v*" - workflow_dispatch: - -permissions: - contents: write - packages: write - -env: - CARGO_TERM_COLOR: always - -jobs: - # ── Tauri Desktop App (Windows + macOS + Linux) ─────────────────────────── - # Produces: .msi, .exe (Windows) | .dmg, .app (macOS) | .AppImage, .deb (Linux) - # Also generates and uploads latest.json (the auto-updater manifest) - desktop: - name: Desktop / ${{ matrix.platform.name }} - strategy: - fail-fast: false - matrix: - platform: - - name: Linux x86_64 - os: ubuntu-22.04 - args: "--target x86_64-unknown-linux-gnu" - rust_target: x86_64-unknown-linux-gnu - - - name: macOS x86_64 - os: macos-latest - args: "--target x86_64-apple-darwin" - rust_target: x86_64-apple-darwin - - - name: macOS ARM64 - os: macos-latest - args: "--target aarch64-apple-darwin" - rust_target: aarch64-apple-darwin - - - name: Windows x86_64 - os: windows-latest - args: "--target x86_64-pc-windows-msvc" - rust_target: x86_64-pc-windows-msvc - - - name: Windows ARM64 - os: windows-latest - args: "--target aarch64-pc-windows-msvc" - rust_target: aarch64-pc-windows-msvc - - runs-on: ${{ matrix.platform.os }} - steps: - - uses: actions/checkout@v4 - - - name: Install system deps (Linux) - if: runner.os == 'Linux' - run: | - sudo apt-get update - sudo apt-get install -y \ - libwebkit2gtk-4.1-dev \ - libgtk-3-dev \ - libayatana-appindicator3-dev \ - librsvg2-dev \ - patchelf - - - uses: dtolnay/rust-toolchain@stable - with: - targets: ${{ matrix.platform.rust_target }} - - - uses: Swatinem/rust-cache@v2 - with: - key: desktop-${{ matrix.platform.rust_target }} - - - name: Import macOS signing certificate - if: runner.os == 'macOS' - env: - MAC_CERT_BASE64: ${{ secrets.MAC_CERT_BASE64 }} - MAC_CERT_PASSWORD: ${{ secrets.MAC_CERT_PASSWORD }} - run: | - echo "$MAC_CERT_BASE64" | base64 --decode > $RUNNER_TEMP/certificate.p12 - KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db - KEYCHAIN_PASSWORD=$(openssl rand -base64 32) - security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" - security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" - security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" - security import $RUNNER_TEMP/certificate.p12 -P "$MAC_CERT_PASSWORD" \ - -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH" - security list-keychain -d user -s "$KEYCHAIN_PATH" - security set-key-partition-list -S apple-tool:,apple:,codesign: \ - -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" - IDENTITY=$(security find-identity -v -p codesigning "$KEYCHAIN_PATH" | grep "Developer ID Application" | head -1 | awk -F'"' '{print $2}') - echo "Using signing identity: $IDENTITY" - echo "APPLE_SIGNING_IDENTITY=$IDENTITY" >> $GITHUB_ENV - rm -f $RUNNER_TEMP/certificate.p12 - - - name: Build and bundle Tauri desktop app - uses: tauri-apps/tauri-action@v0 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} - TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} - APPLE_SIGNING_IDENTITY: ${{ env.APPLE_SIGNING_IDENTITY }} - APPLE_ID: ${{ secrets.MAC_NOTARIZE_APPLE_ID }} - APPLE_PASSWORD: ${{ secrets.MAC_NOTARIZE_PASSWORD }} - APPLE_TEAM_ID: ${{ secrets.MAC_NOTARIZE_TEAM_ID }} - with: - tagName: ${{ github.ref_name }} - releaseName: "OpenFang ${{ github.ref_name }}" - releaseBody: | - ## What's New - - See the [CHANGELOG](https://github.com/RightNow-AI/openfang/blob/main/CHANGELOG.md) for full details. - - ## Installation - - **Desktop App** — Download the installer for your platform below. - - **CLI (Linux/macOS)**: - ```bash - curl -sSf https://openfang.sh | sh - ``` - - **Docker**: - ```bash - docker pull ghcr.io/rightnow-ai/openfang:latest - ``` - - **Coming from OpenClaw?** - ```bash - openfang migrate --from openclaw - ``` - releaseDraft: false - prerelease: false - includeUpdaterJson: true - projectPath: crates/openfang-desktop - args: ${{ matrix.platform.args }} - - # ── CLI Binary (7 platforms + native ARM64 for Android/proot) ──────────── - cli: - name: CLI / ${{ matrix.target }} - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - include: - - target: x86_64-unknown-linux-gnu - os: ubuntu-22.04 - archive: tar.gz - native: false - - - target: aarch64-unknown-linux-gnu - os: ubuntu-22.04 - archive: tar.gz - native: false - - # Native ARM64 build on Ubuntu 24 — links against OpenSSL 3.x - # Works in proot-distro Ubuntu 24 and nix-on-droid without OpenSSL errors - - target: aarch64-unknown-linux-gnu - os: ubuntu-24.04-arm - archive: tar.gz - native: true - artifact_name: aarch64-unknown-linux-gnu-native - - # Static musl build — works on Android nix-on-droid without any libc - - target: aarch64-unknown-linux-musl - os: ubuntu-22.04 - archive: tar.gz - native: false - - - target: x86_64-apple-darwin - os: macos-latest - archive: tar.gz - native: false - - - target: aarch64-apple-darwin - os: macos-latest - archive: tar.gz - native: false - - - target: x86_64-pc-windows-msvc - os: windows-latest - archive: zip - native: false - - - target: aarch64-pc-windows-msvc - os: windows-latest - archive: zip - native: false - - steps: - - uses: actions/checkout@v4 - - - uses: dtolnay/rust-toolchain@stable - with: - targets: ${{ matrix.target }} - - - name: Install build deps (Linux) - if: runner.os == 'Linux' - run: | - sudo apt-get update - sudo apt-get install -y \ - build-essential pkg-config clang cmake perl curl \ - libssl-dev libsqlite3-dev - - - name: Install musl cross-compiler (aarch64-musl) - if: matrix.target == 'aarch64-unknown-linux-musl' - run: sudo apt-get install -y musl-tools gcc-aarch64-linux-gnu - - - name: Install cross (non-native aarch64) - if: contains(fromJSON('["aarch64-unknown-linux-gnu","aarch64-unknown-linux-musl"]'), matrix.target) && matrix.native == false - run: cargo install cross --locked - - - uses: Swatinem/rust-cache@v2 - with: - key: cli-${{ matrix.target }}-${{ matrix.os }} - - # Native ARM64 build — same approach as build-openfang-full-arm workflow - - name: Build CLI (native ARM64) - if: matrix.native == true - env: - CARGO_BUILD_JOBS: "2" - RUSTFLAGS: "-C debuginfo=0" - run: cargo build --release --locked --bin openfang - - # Cross-compiled aarch64 builds - - name: Build CLI (cross) - if: contains(fromJSON('["aarch64-unknown-linux-gnu","aarch64-unknown-linux-musl"]'), matrix.target) && matrix.native == false - env: - OPENSSL_STATIC: "1" - OPENSSL_VENDORED: "1" - run: cross build --release --target ${{ matrix.target }} --bin openfang - - # All other platforms - - name: Build CLI - if: matrix.native == false && matrix.target != 'aarch64-unknown-linux-gnu' && matrix.target != 'aarch64-unknown-linux-musl' - run: cargo build --release --target ${{ matrix.target }} --bin openfang - - - name: Ad-hoc codesign CLI binary (macOS) - if: runner.os == 'macOS' - run: codesign --force --sign - target/${{ matrix.target }}/release/openfang - - - name: Set artifact name - id: artifact - run: | - if [ -n "${{ matrix.artifact_name }}" ]; then - echo "name=${{ matrix.artifact_name }}" >> $GITHUB_OUTPUT - else - echo "name=${{ matrix.target }}" >> $GITHUB_OUTPUT - fi - shell: bash - - - name: Set binary path - id: binpath - run: | - if [ "${{ matrix.native }}" = "true" ]; then - echo "path=target/release/openfang" >> $GITHUB_OUTPUT - else - echo "path=target/${{ matrix.target }}/release/openfang" >> $GITHUB_OUTPUT - fi - shell: bash - - - name: Package (Unix) - if: matrix.archive == 'tar.gz' - run: | - cp ${{ steps.binpath.outputs.path }} openfang - tar czf openfang-${{ steps.artifact.outputs.name }}.tar.gz openfang - sha256sum openfang-${{ steps.artifact.outputs.name }}.tar.gz > openfang-${{ steps.artifact.outputs.name }}.tar.gz.sha256 - - - name: Package (Windows) - if: matrix.archive == 'zip' - shell: pwsh - run: | - Compress-Archive -Path "target/${{ matrix.target }}/release/openfang.exe" -DestinationPath "openfang-${{ matrix.target }}.zip" - $hash = (Get-FileHash "openfang-${{ matrix.target }}.zip" -Algorithm SHA256).Hash.ToLower() - "$hash openfang-${{ matrix.target }}.zip" | Out-File -Encoding ASCII "openfang-${{ matrix.target }}.zip.sha256" - - - name: Upload to GitHub Release - uses: softprops/action-gh-release@v2 - with: - files: openfang-${{ steps.artifact.outputs.name }}.* - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - # ── Docker (linux/amd64 + linux/arm64) ──────────────────────────────────── - docker: - name: Docker Image - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Log in to GHCR - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Set up QEMU (for arm64 emulation) - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Extract version - id: version - run: echo "version=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_OUTPUT" - - name: Build and push (multi-arch) - uses: docker/build-push-action@v6 - with: - context: . - push: true - platforms: linux/amd64,linux/arm64 - tags: | - ghcr.io/rightnow-ai/openfang:latest - ghcr.io/rightnow-ai/openfang:${{ steps.version.outputs.version }} - cache-from: type=gha - cache-to: type=gha,mode=max From b0b85ceda45acec0bc12fcdeea5b4d8f0d6c8aaa Mon Sep 17 00:00:00 2001 From: 1BigBear <181850822+1BigBear@users.noreply.github.com> Date: Sun, 8 Mar 2026 16:43:57 +0300 Subject: [PATCH 07/10] Delete .github/workflows/build-openfang-full-arm.yml --- .github/workflows/build-openfang-full-arm.yml | 47 ------------------- 1 file changed, 47 deletions(-) delete mode 100644 .github/workflows/build-openfang-full-arm.yml diff --git a/.github/workflows/build-openfang-full-arm.yml b/.github/workflows/build-openfang-full-arm.yml deleted file mode 100644 index b4923c04c..000000000 --- a/.github/workflows/build-openfang-full-arm.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: Build OpenFang Full (ARM Ubuntu) - -on: - workflow_dispatch: - -jobs: - build: - runs-on: ubuntu-24.04-arm - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: System deps (build) - run: | - sudo apt-get update - sudo apt-get install -y \ - build-essential pkg-config clang cmake perl git curl \ - libssl-dev libsqlite3-dev \ - libgtk-3-dev libayatana-appindicator3-dev libwebkit2gtk-4.1-dev - - - name: Rust toolchain - uses: dtolnay/rust-toolchain@stable - - - name: Build full workspace - env: - CARGO_BUILD_JOBS: "2" - RUSTFLAGS: "-C debuginfo=0" - run: | - cargo build --release --workspace --locked - - - name: Collect release binaries - run: | - mkdir -p out/bin - find target/release -maxdepth 1 -type f -executable \ - ! -name "*.d" ! -name "build-script-*" -exec cp {} out/bin/ \; - ls -lah out/bin - - - name: Package - run: | - tar -C out -czf openfang-full-aarch64-ubuntu24.tar.gz bin - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: openfang-full-aarch64-ubuntu24 - path: openfang-full-aarch64-ubuntu24.tar.gz From dd40758d257dbde3f9afd92c4c34b787ace60149 Mon Sep 17 00:00:00 2001 From: 1BigBear <181850822+1BigBear@users.noreply.github.com> Date: Sun, 8 Mar 2026 16:51:06 +0300 Subject: [PATCH 08/10] Add files via upload --- .github/workflows/build-openfang-full-arm.yml | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 .github/workflows/build-openfang-full-arm.yml diff --git a/.github/workflows/build-openfang-full-arm.yml b/.github/workflows/build-openfang-full-arm.yml new file mode 100644 index 000000000..01ebaff6e --- /dev/null +++ b/.github/workflows/build-openfang-full-arm.yml @@ -0,0 +1,69 @@ +name: Build OpenFang Full (ARM Ubuntu) + +on: + workflow_dispatch: + push: + tags: + - "v*" + +permissions: + contents: write + +jobs: + build: + runs-on: ubuntu-24.04-arm + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: System deps (build) + run: | + sudo apt-get update + sudo apt-get install -y \ + build-essential pkg-config clang cmake perl git curl \ + libssl-dev libsqlite3-dev \ + libgtk-3-dev libayatana-appindicator3-dev libwebkit2gtk-4.1-dev + + - name: Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - uses: Swatinem/rust-cache@v2 + with: + key: arm-ubuntu24 + + - name: Build full workspace + env: + CARGO_BUILD_JOBS: "2" + RUSTFLAGS: "-C debuginfo=0" + run: | + cargo build --release --workspace --locked + + - name: Collect release binaries + run: | + mkdir -p out/bin + find target/release -maxdepth 1 -type f -executable \ + ! -name "*.d" ! -name "build-script-*" -exec cp {} out/bin/ \; + ls -lah out/bin + + - name: Package + run: | + tar -C out -czf openfang-aarch64-unknown-linux-gnu-native.tar.gz bin + sha256sum openfang-aarch64-unknown-linux-gnu-native.tar.gz > openfang-aarch64-unknown-linux-gnu-native.tar.gz.sha256 + + - name: Upload artifact (manual run) + if: github.event_name == 'workflow_dispatch' + uses: actions/upload-artifact@v4 + with: + name: openfang-aarch64-unknown-linux-gnu-native + path: openfang-aarch64-unknown-linux-gnu-native.tar.gz + + - name: Upload to GitHub Release (tag run) + if: startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@v2 + with: + files: | + openfang-aarch64-unknown-linux-gnu-native.tar.gz + openfang-aarch64-unknown-linux-gnu-native.tar.gz.sha256 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 0a6fd20eea3ca250bcbb89cd8a73af3314f94248 Mon Sep 17 00:00:00 2001 From: 1BigBear <181850822+1BigBear@users.noreply.github.com> Date: Sun, 8 Mar 2026 17:47:46 +0300 Subject: [PATCH 09/10] Delete install.sh --- install.sh | 177 ----------------------------------------------------- 1 file changed, 177 deletions(-) delete mode 100644 install.sh diff --git a/install.sh b/install.sh deleted file mode 100644 index 8618a03ee..000000000 --- a/install.sh +++ /dev/null @@ -1,177 +0,0 @@ -#!/bin/sh -set -eu - -# OpenFang Installer (1BigBear fork - with musl/Android/proot support) -# https://raw.githubusercontent.com/1BigBear/openfang/main/install.sh - -REPO="1BigBear/openfang" -INSTALL_DIR="$HOME/.openfang" -BIN_DIR="$HOME/.openfang/bin" -BINARY="openfang" - -main() { - need_cmd curl - need_cmd tar - need_cmd uname - - _os="$(uname -s)" - _arch="$(uname -m)" - - # Detect environment on Linux aarch64 - _libc="gnu" - _native="false" - if [ "$_os" = "Linux" ]; then - # Check for musl (nix-on-droid) - if ldd /bin/sh 2>&1 | grep -q musl; then - _libc="musl" - # Check for proot-distro Ubuntu 24 — use native build with OpenSSL 3.x - elif [ -f /etc/os-release ]; then - _ubuntu_ver="$(grep '^VERSION_ID' /etc/os-release 2>/dev/null | cut -d'"' -f2)" - if [ "$_ubuntu_ver" = "24.04" ] || [ "$_ubuntu_ver" = "24.10" ]; then - _native="true" - fi - fi - fi - - case "$_os" in - Linux) - case "$_arch" in - x86_64|amd64) - _target="x86_64-unknown-linux-gnu" - ;; - aarch64|arm64) - if [ "$_libc" = "musl" ]; then - # nix-on-droid — static musl binary, no libc dependency - _target="aarch64-unknown-linux-musl" - elif [ "$_native" = "true" ]; then - # proot-distro Ubuntu 24 — native build with OpenSSL 3.x - _target="aarch64-unknown-linux-gnu-native" - else - # fallback gnu build - _target="aarch64-unknown-linux-gnu" - fi - ;; - *) - err "Unsupported architecture: $_arch" - ;; - esac - ;; - Darwin) - case "$_arch" in - x86_64|amd64) _target="x86_64-apple-darwin" ;; - aarch64|arm64) _target="aarch64-apple-darwin" ;; - *) err "Unsupported architecture: $_arch" ;; - esac - ;; - *) - err "Unsupported OS: $_os (use irm https://openfang.sh/install.ps1 | iex on Windows)" - ;; - esac - - _url="https://github.com/${REPO}/releases/latest/download/openfang-${_target}.tar.gz" - - say "Detected: $_os $_arch -> $_target" - say "Downloading from: $_url" - - _tmpdir="$(mktemp -d 2>/dev/null || mktemp -d -t openfang)" - trap 'rm -rf "$_tmpdir"' EXIT - - _code=$(curl -fsSL -w "%{http_code}" "$_url" -o "${_tmpdir}/openfang.tar.gz") || true - if [ "$_code" = "404" ]; then - err "Release not found for ${_target}. Check https://github.com/${REPO}/releases" - fi - if [ ! -f "${_tmpdir}/openfang.tar.gz" ] || [ "$(wc -c < "${_tmpdir}/openfang.tar.gz")" -lt 1000 ]; then - err "Download failed (HTTP ${_code}). Check https://github.com/${REPO}/releases" - fi - - say "Extracting..." - tar -xzf "${_tmpdir}/openfang.tar.gz" -C "$_tmpdir" - - # Find the binary - _bin="$(find "$_tmpdir" -name "$BINARY" -type f -perm +111 2>/dev/null | head -1)" - if [ -z "$_bin" ]; then - _bin="$(find "$_tmpdir" -name "$BINARY" -type f | head -1)" - fi - if [ -z "$_bin" ]; then - err "Could not find openfang binary in archive" - fi - - mkdir -p "$BIN_DIR" - cp "$_bin" "${BIN_DIR}/${BINARY}" - chmod +x "${BIN_DIR}/${BINARY}" - - # Verify it runs - if "${BIN_DIR}/${BINARY}" --version >/dev/null 2>&1; then - _ver=$("${BIN_DIR}/${BINARY}" --version 2>/dev/null || echo "unknown") - say "Installed: $_ver" - else - say "Installed to: ${BIN_DIR}/${BINARY}" - fi - - add_to_path - - say "" - say "OpenFang installed successfully!" - say "" - say " Run: openfang init" - say " Docs: https://openfang.sh/docs" - say "" - - # Check if binary is reachable - if ! command -v openfang >/dev/null 2>&1; then - say "Note: restart your shell or run:" - say " export PATH=\"${BIN_DIR}:\$PATH\"" - say "" - fi -} - -add_to_path() { - # Skip if already in PATH - case ":$PATH:" in - *":${BIN_DIR}:"*) return ;; - esac - - _line="export PATH=\"${BIN_DIR}:\$PATH\"" - - # Detect shell profile - _profile="" - if [ -n "${SHELL:-}" ]; then - case "$SHELL" in - */zsh) _profile="$HOME/.zshrc" ;; - */bash) - if [ -f "$HOME/.bashrc" ]; then - _profile="$HOME/.bashrc" - else - _profile="$HOME/.bash_profile" - fi ;; - */fish) _profile="$HOME/.config/fish/config.fish" ;; - *) _profile="$HOME/.profile" ;; - esac - else - _profile="$HOME/.profile" - fi - - if [ -n "$_profile" ] && [ -f "$_profile" ]; then - if ! grep -q "/.openfang/bin" "$_profile" 2>/dev/null; then - printf "\n# OpenFang\n%s\n" "$_line" >> "$_profile" - say "Added to PATH via $_profile" - fi - fi -} - -say() { - printf " \033[1;36mopenfang\033[0m %s\n" "$1" -} - -err() { - printf " \033[1;36mopenfang\033[0m \033[1;31merror:\033[0m %s\n" "$1" >&2 - exit 1 -} - -need_cmd() { - if ! command -v "$1" >/dev/null 2>&1; then - err "need '$1' (command not found)" - fi -} - -main From 3b436ab24efd704d042f7b752f0cefda2b562193 Mon Sep 17 00:00:00 2001 From: 1BigBear <181850822+1BigBear@users.noreply.github.com> Date: Sun, 8 Mar 2026 17:49:54 +0300 Subject: [PATCH 10/10] Add files via upload --- install.sh | 147 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 install.sh diff --git a/install.sh b/install.sh new file mode 100644 index 000000000..925626bc5 --- /dev/null +++ b/install.sh @@ -0,0 +1,147 @@ +#!/bin/sh +set -eu + +# OpenFang Installer (1BigBear fork - proot-distro ARM64 support) +# https://raw.githubusercontent.com/1BigBear/openfang/main/install.sh + +REPO="1BigBear/openfang" +INSTALL_DIR="$HOME/.openfang" +BIN_DIR="$HOME/.openfang/bin" +BINARY="openfang" + +main() { + need_cmd curl + need_cmd tar + need_cmd uname + + _os="$(uname -s)" + _arch="$(uname -m)" + + case "$_os" in + Linux) + case "$_arch" in + x86_64|amd64) + _target="x86_64-unknown-linux-gnu" + ;; + aarch64|arm64) + _target="aarch64-unknown-linux-gnu-native" + ;; + *) + err "Unsupported architecture: $_arch" + ;; + esac + ;; + Darwin) + case "$_arch" in + x86_64|amd64) _target="x86_64-apple-darwin" ;; + aarch64|arm64) _target="aarch64-apple-darwin" ;; + *) err "Unsupported architecture: $_arch" ;; + esac + ;; + *) + err "Unsupported OS: $_os" + ;; + esac + + _url="https://github.com/${REPO}/releases/latest/download/openfang-${_target}.tar.gz" + + say "Detected: $_os $_arch -> $_target" + say "Downloading from: $_url" + + _tmpdir="$(mktemp -d 2>/dev/null || mktemp -d -t openfang)" + trap 'rm -rf "$_tmpdir"' EXIT + + _code=$(curl -fsSL -w "%{http_code}" "$_url" -o "${_tmpdir}/openfang.tar.gz") || true + if [ "$_code" = "404" ]; then + err "Release not found. Check https://github.com/${REPO}/releases" + fi + if [ ! -f "${_tmpdir}/openfang.tar.gz" ] || [ "$(wc -c < "${_tmpdir}/openfang.tar.gz")" -lt 1000 ]; then + err "Download failed (HTTP ${_code}). Check https://github.com/${REPO}/releases" + fi + + say "Extracting..." + tar -xzf "${_tmpdir}/openfang.tar.gz" -C "$_tmpdir" + + _bin="$(find "$_tmpdir" -name "$BINARY" -type f -perm +111 2>/dev/null | head -1)" + if [ -z "$_bin" ]; then + _bin="$(find "$_tmpdir" -name "$BINARY" -type f | head -1)" + fi + if [ -z "$_bin" ]; then + err "Could not find openfang binary in archive" + fi + + mkdir -p "$BIN_DIR" + cp "$_bin" "${BIN_DIR}/${BINARY}" + chmod +x "${BIN_DIR}/${BINARY}" + + if "${BIN_DIR}/${BINARY}" --version >/dev/null 2>&1; then + _ver=$("${BIN_DIR}/${BINARY}" --version 2>/dev/null || echo "unknown") + say "Installed: $_ver" + else + say "Installed to: ${BIN_DIR}/${BINARY}" + fi + + add_to_path + + say "" + say "OpenFang installed successfully!" + say "" + say " Run: openfang init" + say " Docs: https://github.com/${REPO}" + say "" + + if ! command -v openfang >/dev/null 2>&1; then + say "Note: restart your shell or run:" + say " export PATH=\"${BIN_DIR}:\$PATH\"" + say "" + fi +} + +add_to_path() { + case ":$PATH:" in + *":${BIN_DIR}:"*) return ;; + esac + + _line="export PATH=\"${BIN_DIR}:\$PATH\"" + + _profile="" + if [ -n "${SHELL:-}" ]; then + case "$SHELL" in + */zsh) _profile="$HOME/.zshrc" ;; + */bash) + if [ -f "$HOME/.bashrc" ]; then + _profile="$HOME/.bashrc" + else + _profile="$HOME/.bash_profile" + fi ;; + */fish) _profile="$HOME/.config/fish/config.fish" ;; + *) _profile="$HOME/.profile" ;; + esac + else + _profile="$HOME/.profile" + fi + + if [ -n "$_profile" ] && [ -f "$_profile" ]; then + if ! grep -q "/.openfang/bin" "$_profile" 2>/dev/null; then + printf "\n# OpenFang\n%s\n" "$_line" >> "$_profile" + say "Added to PATH via $_profile" + fi + fi +} + +say() { + printf " \033[1;36mopenfang\033[0m %s\n" "$1" +} + +err() { + printf " \033[1;36mopenfang\033[0m \033[1;31merror:\033[0m %s\n" "$1" >&2 + exit 1 +} + +need_cmd() { + if ! command -v "$1" >/dev/null 2>&1; then + err "need '$1' (command not found)" + fi +} + +main