diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index bda61ba..3e47e99 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -21,10 +21,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: '24' registry-url: 'https://registry.npmjs.org' diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml new file mode 100644 index 0000000..42c2247 --- /dev/null +++ b/.github/workflows/release-binaries.yml @@ -0,0 +1,201 @@ +name: Release Binaries + +on: + push: + branches: [main, install-sh] + paths: + - 'package.json' + - '.github/workflows/release-binaries.yml' + - 'nfpm.yaml' + workflow_dispatch: + +permissions: + contents: write + +jobs: + check-version: + runs-on: ubuntu-latest + name: Check for version bump + outputs: + version: ${{ steps.check.outputs.version }} + released: ${{ steps.check.outputs.released }} + dry_run: ${{ steps.check.outputs.dry_run }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Check if version tag exists + id: check + run: | + VERSION=$(node -p "require('./package.json').version") + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + # Dry run on non-main branches + if [ "${{ github.ref_name }}" != "main" ]; then + echo "dry_run=true" >> "$GITHUB_OUTPUT" + echo "released=false" >> "$GITHUB_OUTPUT" + echo "Dry run on branch ${{ github.ref_name }} — will build but not release" + elif git rev-parse "v$VERSION" >/dev/null 2>&1; then + echo "dry_run=false" >> "$GITHUB_OUTPUT" + echo "released=true" >> "$GITHUB_OUTPUT" + echo "v$VERSION already released — skipping" + else + echo "dry_run=false" >> "$GITHUB_OUTPUT" + echo "released=false" >> "$GITHUB_OUTPUT" + echo "New version detected: v$VERSION" + fi + + build: + needs: check-version + if: needs.check-version.outputs.released == 'false' + strategy: + matrix: + include: + - os: ubuntu-latest + target: bun-linux-x64 + binary: firecrawl-linux-x64 + - os: ubuntu-latest + target: bun-linux-arm64 + binary: firecrawl-linux-arm64 + - os: macos-latest + target: bun-darwin-arm64 + binary: firecrawl-darwin-arm64 + - os: macos-latest + target: bun-darwin-x64 + binary: firecrawl-darwin-x64 + - os: ubuntu-latest + target: bun-windows-x64 + binary: firecrawl-windows-x64.exe + + runs-on: ${{ matrix.os }} + name: Build ${{ matrix.binary }} + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Build binary + run: bun build src/index.ts --compile --target=${{ matrix.target }} --outfile ${{ matrix.binary }} + + - name: Create tarball (Unix) + if: "!contains(matrix.binary, 'windows')" + run: tar -czf ${{ matrix.binary }}.tar.gz ${{ matrix.binary }} + + - name: Create zip (Windows) + if: contains(matrix.binary, 'windows') + run: zip ${{ matrix.binary }}.zip ${{ matrix.binary }} + + - name: Upload binary artifact + uses: actions/upload-artifact@v7 + with: + name: ${{ matrix.binary }} + path: | + ${{ matrix.binary }} + ${{ matrix.binary }}.tar.gz + ${{ matrix.binary }}.zip + if-no-files-found: ignore + + release: + needs: [check-version, build] + runs-on: ubuntu-latest + name: Create release and upload assets + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Download all artifacts + uses: actions/download-artifact@v8 + with: + path: artifacts + + - name: Collect release files + run: | + mkdir -p release binaries + find artifacts -type f \( -name "*.tar.gz" -o -name "*.zip" \) -exec cp {} release/ \; + # Copy raw binaries for nfpm packaging + for dir in artifacts/firecrawl-linux-*/; do + find "$dir" -type f ! -name "*.tar.gz" ! -name "*.zip" -exec cp {} binaries/ \; + done + chmod +x binaries/* + ls -la release/ + ls -la binaries/ + + - name: Install nfpm + run: | + NFPM_VERSION=$(gh release view --repo goreleaser/nfpm --json tagName --jq '.tagName' | sed 's/^v//') + curl -sfL "https://github.com/goreleaser/nfpm/releases/download/v${NFPM_VERSION}/nfpm_${NFPM_VERSION}_Linux_x86_64.tar.gz" | tar -xz -C /usr/local/bin nfpm + nfpm --version + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Build Linux packages + run: | + VERSION="${{ needs.check-version.outputs.version }}" + + # linux amd64 + export BINARY=binaries/firecrawl-linux-x64 VERSION="$VERSION" ARCH=amd64 + envsubst < nfpm.yaml > /tmp/nfpm-amd64.yaml + for fmt in deb rpm apk archlinux; do + nfpm package --config /tmp/nfpm-amd64.yaml --packager "$fmt" --target release/ + done + + # linux arm64 + export BINARY=binaries/firecrawl-linux-arm64 ARCH=arm64 + envsubst < nfpm.yaml > /tmp/nfpm-arm64.yaml + for fmt in deb rpm apk archlinux; do + nfpm package --config /tmp/nfpm-arm64.yaml --packager "$fmt" --target release/ + done + + ls -la release/ + + - name: Generate checksums + run: | + cd release + sha256sum * > checksums.txt + cat checksums.txt + + - name: Summary (dry run) + if: needs.check-version.outputs.dry_run == 'true' + run: | + echo "## Dry Run — Release v${{ needs.check-version.outputs.version }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "All artifacts built and packaged successfully:" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + ls -lh release/ >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**No release created — this is a dry run on branch \`${{ github.ref_name }}\`.**" >> $GITHUB_STEP_SUMMARY + + - name: Generate release notes + if: needs.check-version.outputs.dry_run != 'true' + run: | + PREV_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + if [ -n "$PREV_TAG" ]; then + git log --pretty=format:"- %s" "$PREV_TAG..HEAD" -- | head -50 > /tmp/release-notes.md + else + echo "Initial release" > /tmp/release-notes.md + fi + + - name: Create tag and GitHub release + if: needs.check-version.outputs.dry_run != 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + VERSION="v${{ needs.check-version.outputs.version }}" + git tag "$VERSION" + git push origin "$VERSION" + gh release create "$VERSION" release/* \ + --repo "${{ github.repository }}" \ + --title "Firecrawl CLI $VERSION" \ + --notes-file /tmp/release-notes.md diff --git a/.github/workflows/sync-plugin.yml b/.github/workflows/sync-plugin.yml index bf63e59..36728be 100644 --- a/.github/workflows/sync-plugin.yml +++ b/.github/workflows/sync-plugin.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout CLI repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: path: cli diff --git a/.github/workflows/sync-skills.yml b/.github/workflows/sync-skills.yml index 8695101..f5b1331 100644 --- a/.github/workflows/sync-skills.yml +++ b/.github/workflows/sync-skills.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout CLI repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: path: cli diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 66426cf..80859fb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,10 +13,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: '18' @@ -39,3 +39,48 @@ jobs: - name: Run tests run: pnpm run test + + test-binary: + if: github.event_name == 'pull_request' + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + target: bun-linux-x64 + binary: firecrawl-linux-x64 + - os: macos-latest + target: bun-darwin-arm64 + binary: firecrawl-darwin-arm64 + - os: windows-latest + target: bun-windows-x64 + binary: firecrawl-windows-x64.exe + + runs-on: ${{ matrix.os }} + name: Binary test (${{ matrix.os }}) + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Compile binary + run: bun build src/index.ts --compile --target=${{ matrix.target }} --outfile ${{ matrix.binary }} + + - name: Verify binary runs (Unix) + if: runner.os != 'Windows' + run: | + chmod +x ${{ matrix.binary }} + ./${{ matrix.binary }} --version + ./${{ matrix.binary }} --help + + - name: Verify binary runs (Windows) + if: runner.os == 'Windows' + run: | + .\${{ matrix.binary }} --version + .\${{ matrix.binary }} --help diff --git a/homebrew/firecrawl-cli.rb b/homebrew/firecrawl-cli.rb new file mode 100644 index 0000000..73b5b1a --- /dev/null +++ b/homebrew/firecrawl-cli.rb @@ -0,0 +1,47 @@ +class FirecrawlCli < Formula + desc "CLI for Firecrawl - web scraping, search, and browser automation" + homepage "https://firecrawl.dev" + version "1.10.0" + license "ISC" + + on_macos do + on_arm do + url "https://github.com/firecrawl/cli/releases/download/v#{version}/firecrawl-darwin-arm64.tar.gz" + sha256 "PLACEHOLDER" + end + on_intel do + url "https://github.com/firecrawl/cli/releases/download/v#{version}/firecrawl-darwin-x64.tar.gz" + sha256 "PLACEHOLDER" + end + end + on_linux do + on_arm do + url "https://github.com/firecrawl/cli/releases/download/v#{version}/firecrawl-linux-arm64.tar.gz" + sha256 "PLACEHOLDER" + end + on_intel do + url "https://github.com/firecrawl/cli/releases/download/v#{version}/firecrawl-linux-x64.tar.gz" + sha256 "PLACEHOLDER" + end + end + + def install + if OS.mac? + if Hardware::CPU.arm? + bin.install "firecrawl-darwin-arm64" => "firecrawl" + else + bin.install "firecrawl-darwin-x64" => "firecrawl" + end + elsif OS.linux? + if Hardware::CPU.arm? + bin.install "firecrawl-linux-arm64" => "firecrawl" + else + bin.install "firecrawl-linux-x64" => "firecrawl" + end + end + end + + test do + assert_match version.to_s, shell_output("#{bin}/firecrawl --version") + end +end diff --git a/nfpm.yaml b/nfpm.yaml new file mode 100644 index 0000000..c784a4a --- /dev/null +++ b/nfpm.yaml @@ -0,0 +1,14 @@ +name: firecrawl +description: CLI for Firecrawl - web scraping, search, and browser automation +homepage: https://firecrawl.dev +maintainer: Firecrawl +license: ISC +vendor: Firecrawl +version: ${VERSION} +arch: ${ARCH} + +contents: + - src: ${BINARY} + dst: /usr/bin/firecrawl + file_info: + mode: 0755 diff --git a/package.json b/package.json index ae708d7..1f07c6a 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,11 @@ "test:watch": "vitest", "test": "vitest run", "publish-beta": "npm publish --tag beta", - "publish-prod": "npm publish --access public" + "publish-prod": "npm publish --access public", + "build:binary": "bash scripts/build-binaries.sh", + "build:binary:darwin": "bash scripts/build-binaries.sh darwin", + "build:binary:linux": "bash scripts/build-binaries.sh linux", + "build:binary:windows": "bash scripts/build-binaries.sh windows" }, "lint-staged": { "*.{ts,json,md}": [ diff --git a/scripts/build-binaries.sh b/scripts/build-binaries.sh new file mode 100755 index 0000000..74173ec --- /dev/null +++ b/scripts/build-binaries.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Build standalone Firecrawl CLI binaries for all platforms using Bun +# +# Usage: +# ./scripts/build-binaries.sh # build all targets +# ./scripts/build-binaries.sh darwin # build only macOS targets +# ./scripts/build-binaries.sh linux # build only Linux targets +# ./scripts/build-binaries.sh windows # build only Windows targets + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +ENTRY="$ROOT_DIR/src/index.ts" +OUT_DIR="$ROOT_DIR/dist/bin" + +mkdir -p "$OUT_DIR" + +TARGETS=( + "bun-darwin-arm64:firecrawl-darwin-arm64" + "bun-darwin-x64:firecrawl-darwin-x64" + "bun-linux-x64:firecrawl-linux-x64" + "bun-linux-arm64:firecrawl-linux-arm64" + "bun-windows-x64:firecrawl-windows-x64.exe" +) + +FILTER="${1:-all}" + +build_target() { + local target="$1" + local outfile="$2" + echo "Building $outfile (target: $target)..." + bun build "$ENTRY" --compile --target="$target" --outfile "$OUT_DIR/$outfile" + echo " -> $OUT_DIR/$outfile" +} + +for entry in "${TARGETS[@]}"; do + target="${entry%%:*}" + outfile="${entry##*:}" + + case "$FILTER" in + all) build_target "$target" "$outfile" ;; + darwin) [[ "$target" == *darwin* ]] && build_target "$target" "$outfile" ;; + linux) [[ "$target" == *linux* ]] && build_target "$target" "$outfile" ;; + windows) [[ "$target" == *windows* ]] && build_target "$target" "$outfile" ;; + *) echo "Unknown filter: $FILTER (use all, darwin, linux, windows)"; exit 1 ;; + esac +done + +# Generate checksums +echo "" +echo "Generating checksums..." +cd "$OUT_DIR" +shasum -a 256 firecrawl-* > checksums.txt 2>/dev/null || sha256sum firecrawl-* > checksums.txt +echo "Checksums written to $OUT_DIR/checksums.txt" +cat checksums.txt + +# Build Linux packages with nfpm (if available and linux binaries exist) +if command -v nfpm &>/dev/null; then + VERSION="${VERSION:-$(node -p "require('$ROOT_DIR/package.json').version")}" + PKG_DIR="$OUT_DIR/packages" + mkdir -p "$PKG_DIR" + + for arch_pair in "x64:amd64" "arm64:arm64"; do + bun_arch="${arch_pair%%:*}" + nfpm_arch="${arch_pair##*:}" + binary="$OUT_DIR/firecrawl-linux-$bun_arch" + + if [[ -f "$binary" ]]; then + echo "" + echo "Packaging linux-$nfpm_arch..." + export BINARY="$binary" VERSION="$VERSION" ARCH="$nfpm_arch" + envsubst < "$ROOT_DIR/nfpm.yaml" > /tmp/nfpm-$nfpm_arch.yaml + for fmt in deb rpm apk archlinux; do + nfpm package --config /tmp/nfpm-$nfpm_arch.yaml --packager "$fmt" --target "$PKG_DIR/" + done + fi + done + + echo "" + echo "Packages built in $PKG_DIR/" + ls -la "$PKG_DIR/" +else + echo "" + echo "nfpm not found — skipping Linux package generation." + echo "Install: curl -sfL https://github.com/goreleaser/nfpm/releases/latest/download/nfpm_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m).tar.gz | tar -xz -C /usr/local/bin nfpm" +fi + +echo "" +echo "Done. Binaries are in $OUT_DIR/" diff --git a/scripts/install.ps1 b/scripts/install.ps1 new file mode 100644 index 0000000..94ac85c --- /dev/null +++ b/scripts/install.ps1 @@ -0,0 +1,114 @@ +# Firecrawl CLI installer for Windows +# Usage: irm https://firecrawl.dev/install.ps1 | iex +# +# Environment variables: +# FIRECRAWL_INSTALL_DIR - Override install directory +# FIRECRAWL_VERSION - Install a specific version (default: latest) + +$ErrorActionPreference = "Stop" + +$Repo = "firecrawl/cli" +$BinaryName = "firecrawl" + +function Write-Info($msg) { Write-Host "info " -ForegroundColor Blue -NoNewline; Write-Host $msg } +function Write-Warn($msg) { Write-Host "warn " -ForegroundColor Yellow -NoNewline; Write-Host $msg } +function Write-Err($msg) { Write-Host "error " -ForegroundColor Red -NoNewline; Write-Host $msg } +function Write-Ok($msg) { Write-Host "success" -ForegroundColor Green -NoNewline; Write-Host " $msg" } + +function Get-LatestVersion { + $url = "https://api.github.com/repos/$Repo/releases/latest" + $release = Invoke-RestMethod -Uri $url -Headers @{ "User-Agent" = "firecrawl-installer" } + return $release.tag_name -replace "^v", "" +} + +function Get-Platform { + $arch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture + switch ($arch) { + "X64" { return "x64" } + "Arm64" { return "arm64" } + default { throw "Unsupported architecture: $arch" } + } +} + +function Install-Firecrawl { + $arch = Get-Platform + Write-Info "Detected platform: windows-$arch" + + # Determine version + if ($env:FIRECRAWL_VERSION) { + $version = $env:FIRECRAWL_VERSION -replace "^v", "" + Write-Info "Installing specified version: v$version" + } else { + Write-Info "Fetching latest version..." + $version = Get-LatestVersion + Write-Info "Latest version: v$version" + } + + # Determine install directory + if ($env:FIRECRAWL_INSTALL_DIR) { + $installDir = $env:FIRECRAWL_INSTALL_DIR + } else { + $installDir = "$env:LOCALAPPDATA\firecrawl\bin" + } + + if (-not (Test-Path $installDir)) { + New-Item -ItemType Directory -Path $installDir -Force | Out-Null + } + + # Construct download URLs + $binaryFile = "$BinaryName-windows-$arch.exe" + $baseUrl = "https://github.com/$Repo/releases/download/v$version" + $binaryUrl = "$baseUrl/$binaryFile" + $checksumUrl = "$baseUrl/checksums.txt" + + # Download to temp directory + $tmpDir = Join-Path ([System.IO.Path]::GetTempPath()) "firecrawl-install-$(Get-Random)" + New-Item -ItemType Directory -Path $tmpDir -Force | Out-Null + + try { + Write-Info "Downloading firecrawl v$version for windows-$arch..." + Invoke-WebRequest -Uri $binaryUrl -OutFile "$tmpDir\firecrawl.exe" -UseBasicParsing + + Write-Info "Downloading checksums..." + Invoke-WebRequest -Uri $checksumUrl -OutFile "$tmpDir\checksums.txt" -UseBasicParsing + + # Verify checksum + $checksums = Get-Content "$tmpDir\checksums.txt" + $expectedLine = $checksums | Where-Object { $_ -match $binaryFile } + if ($expectedLine) { + $expectedHash = ($expectedLine -split "\s+")[0] + $actualHash = (Get-FileHash "$tmpDir\firecrawl.exe" -Algorithm SHA256).Hash.ToLower() + if ($actualHash -ne $expectedHash) { + Write-Err "Checksum mismatch!" + Write-Err " Expected: $expectedHash" + Write-Err " Actual: $actualHash" + exit 1 + } + Write-Info "Checksum verified." + } else { + Write-Warn "No checksum found for $binaryFile — skipping verification" + } + + # Install + Write-Info "Installing to $installDir\firecrawl.exe..." + Copy-Item "$tmpDir\firecrawl.exe" "$installDir\firecrawl.exe" -Force + + # Add to PATH if needed + $userPath = [Environment]::GetEnvironmentVariable("Path", "User") + if ($userPath -notlike "*$installDir*") { + [Environment]::SetEnvironmentVariable("Path", "$installDir;$userPath", "User") + Write-Warn "$installDir added to your PATH. Restart your terminal for changes to take effect." + } + + Write-Host "" + Write-Ok "Firecrawl CLI v$version installed successfully!" + Write-Host "" + Write-Host " Run 'firecrawl --help' to get started." + Write-Host " Run 'firecrawl login' to authenticate with your API key." + Write-Host "" + } finally { + Remove-Item -Recurse -Force $tmpDir -ErrorAction SilentlyContinue + } +} + +Install-Firecrawl diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 0000000..7756a95 --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,213 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Firecrawl CLI installer +# Usage: curl -fsSL https://firecrawl.dev/install.sh | bash +# +# Detects OS/arch, downloads the correct binary from GitHub Releases, +# verifies checksum, and installs to ~/.local/bin (or /usr/local/bin with sudo). +# +# Environment variables: +# FIRECRAWL_INSTALL_DIR - Override install directory (default: ~/.local/bin) +# FIRECRAWL_VERSION - Install a specific version (default: latest) + +REPO="firecrawl/cli" +BINARY_NAME="firecrawl" + +# Colors (disabled if not a terminal) +if [ -t 1 ]; then + RED='\033[0;31m' + GREEN='\033[0;32m' + YELLOW='\033[0;33m' + BLUE='\033[0;34m' + BOLD='\033[1m' + RESET='\033[0m' +else + RED='' GREEN='' YELLOW='' BLUE='' BOLD='' RESET='' +fi + +info() { echo -e "${BLUE}${BOLD}info${RESET} $*"; } +warn() { echo -e "${YELLOW}${BOLD}warn${RESET} $*"; } +error() { echo -e "${RED}${BOLD}error${RESET} $*" >&2; } +success() { echo -e "${GREEN}${BOLD}success${RESET} $*"; } + +detect_os() { + local os + os="$(uname -s)" + case "$os" in + Linux*) echo "linux" ;; + Darwin*) echo "darwin" ;; + MINGW*|MSYS*|CYGWIN*) echo "windows" ;; + *) error "Unsupported OS: $os"; exit 1 ;; + esac +} + +detect_arch() { + local arch + arch="$(uname -m)" + case "$arch" in + x86_64|amd64) echo "x64" ;; + arm64|aarch64) echo "arm64" ;; + *) error "Unsupported architecture: $arch"; exit 1 ;; + esac +} + +get_latest_version() { + local url="https://api.github.com/repos/${REPO}/releases/latest" + local version + + if command -v curl &>/dev/null; then + version=$(curl -fsSL "$url" | grep '"tag_name"' | sed -E 's/.*"tag_name": *"([^"]+)".*/\1/') + elif command -v wget &>/dev/null; then + version=$(wget -qO- "$url" | grep '"tag_name"' | sed -E 's/.*"tag_name": *"([^"]+)".*/\1/') + else + error "Neither curl nor wget found. Please install one and retry." + exit 1 + fi + + if [ -z "$version" ]; then + error "Could not determine latest version. Check https://github.com/${REPO}/releases" + exit 1 + fi + + # Strip leading 'v' if present + echo "${version#v}" +} + +download() { + local url="$1" + local dest="$2" + + if command -v curl &>/dev/null; then + curl -fsSL --progress-bar "$url" -o "$dest" + elif command -v wget &>/dev/null; then + wget -q --show-progress "$url" -O "$dest" + fi +} + +verify_checksum() { + local file="$1" + local expected="$2" + local actual + + if command -v shasum &>/dev/null; then + actual=$(shasum -a 256 "$file" | awk '{print $1}') + elif command -v sha256sum &>/dev/null; then + actual=$(sha256sum "$file" | awk '{print $1}') + else + warn "No SHA256 tool found — skipping checksum verification" + return 0 + fi + + if [ "$actual" != "$expected" ]; then + error "Checksum mismatch!" + error " Expected: $expected" + error " Actual: $actual" + exit 1 + fi +} + +ensure_path() { + local dir="$1" + local shell_name + shell_name="$(basename "${SHELL:-/bin/sh}")" + + case ":$PATH:" in + *":$dir:"*) return ;; # already in PATH + esac + + local rc_file + case "$shell_name" in + zsh) rc_file="$HOME/.zshrc" ;; + bash) rc_file="$HOME/.bashrc" ;; + fish) rc_file="$HOME/.config/fish/config.fish" ;; + *) rc_file="$HOME/.profile" ;; + esac + + echo "" >> "$rc_file" + if [ "$shell_name" = "fish" ]; then + echo "set -gx PATH \"$dir\" \$PATH" >> "$rc_file" + else + echo "export PATH=\"$dir:\$PATH\"" >> "$rc_file" + fi + + warn "$dir was not in your PATH. Added it to $rc_file" + warn "Run 'source $rc_file' or open a new terminal to use firecrawl." +} + +main() { + local os arch version install_dir binary_suffix="" + + os="$(detect_os)" + arch="$(detect_arch)" + + if [ "$os" = "windows" ]; then + error "This script is for macOS/Linux. On Windows, use:" + error " irm https://firecrawl.dev/install.ps1 | iex" + exit 1 + fi + + info "Detected platform: ${os}-${arch}" + + # Determine version + if [ -n "${FIRECRAWL_VERSION:-}" ]; then + version="${FIRECRAWL_VERSION#v}" + info "Installing specified version: v$version" + else + info "Fetching latest version..." + version="$(get_latest_version)" + info "Latest version: v$version" + fi + + # Determine install directory + install_dir="${FIRECRAWL_INSTALL_DIR:-$HOME/.local/bin}" + mkdir -p "$install_dir" + + # Construct download URLs + local binary_name="${BINARY_NAME}-${os}-${arch}" + local base_url="https://github.com/${REPO}/releases/download/v${version}" + local binary_url="${base_url}/${binary_name}.tar.gz" + local checksum_url="${base_url}/checksums.txt" + + # Download to temp directory + local tmp_dir + tmp_dir="$(mktemp -d)" + trap 'rm -rf "$tmp_dir"' EXIT + + info "Downloading firecrawl v${version} for ${os}-${arch}..." + download "$binary_url" "$tmp_dir/firecrawl.tar.gz" + + info "Downloading checksums..." + download "$checksum_url" "$tmp_dir/checksums.txt" + + # Verify checksum + local expected_checksum + expected_checksum=$(grep "${binary_name}.tar.gz" "$tmp_dir/checksums.txt" | awk '{print $1}') + if [ -n "$expected_checksum" ]; then + info "Verifying checksum..." + verify_checksum "$tmp_dir/firecrawl.tar.gz" "$expected_checksum" + else + warn "No checksum found for ${binary_name}.tar.gz — skipping verification" + fi + + # Extract and install + info "Extracting..." + tar -xzf "$tmp_dir/firecrawl.tar.gz" -C "$tmp_dir" + + info "Installing to ${install_dir}/firecrawl..." + mv "$tmp_dir/$binary_name" "$install_dir/firecrawl" 2>/dev/null \ + || mv "$tmp_dir/firecrawl" "$install_dir/firecrawl" + chmod +x "$install_dir/firecrawl" + + # Ensure PATH includes install dir + ensure_path "$install_dir" + + echo "" + success "Firecrawl CLI v${version} installed successfully!" + echo "" + echo " Run 'firecrawl --help' to get started." + echo " Run 'firecrawl login' to authenticate with your API key." + echo "" +} + +main "$@"