/** * Update the Homebrew formula for hapi. * * This script: * 1. Reads checksums from release-artifacts/checksums.txt * 2. Generates an updated hapi.rb formula * 3. Optionally clones the tap repo, commits, and pushes * * Usage: * # Generate formula locally (for review) * bun run scripts/update-homebrew-formula.ts --version 0.1.0 * * # Generate and push to tap repository * bun run scripts/update-homebrew-formula.ts --version 0.1.0 --push * * Environment: * HOMEBREW_TAP_REPO - Git URL of the tap repository (default: https://github.com/tiann/homebrew-tap.git) */ import { execSync } from 'node:child_process'; import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { tmpdir } from 'node:os'; const __dirname = dirname(fileURLToPath(import.meta.url)); const projectRoot = join(__dirname, '..'); interface PlatformSha { darwinArm64: string; darwinX64: string; linuxArm64: string; linuxX64: string; } function parseChecksums(checksumsPath: string): PlatformSha { const content = readFileSync(checksumsPath, 'utf-8'); const lines = content.trim().split('\n'); const shas: Partial = {}; for (const line of lines) { const [sha, filename] = line.split(' '); if (!sha || !filename) continue; if (filename.includes('darwin-arm64')) shas.darwinArm64 = sha; else if (filename.includes('darwin-x64')) shas.darwinX64 = sha; else if (filename.includes('linux-arm64')) shas.linuxArm64 = sha; else if (filename.includes('linux-x64')) shas.linuxX64 = sha; } const missing: string[] = []; if (!shas.darwinArm64) missing.push('darwin-arm64'); if (!shas.darwinX64) missing.push('darwin-x64'); if (!shas.linuxArm64) missing.push('linux-arm64'); if (!shas.linuxX64) missing.push('linux-x64'); if (missing.length > 0) { throw new Error(`Missing SHA256 checksums for: ${missing.join(', ')}`); } return shas as PlatformSha; } function generateFormula(version: string, shas: PlatformSha): string { return `# typed: false # frozen_string_literal: true class Hapi < Formula desc "App for agentic coding - access coding agent anywhere" homepage "https://github.com/tiann/hapi" version "${version}" license "MIT" on_macos do if Hardware::CPU.arm? url "https://github.com/tiann/hapi/releases/download/v#{version}/hapi-darwin-arm64.tar.gz" sha256 "${shas.darwinArm64}" else url "https://github.com/tiann/hapi/releases/download/v#{version}/hapi-darwin-x64.tar.gz" sha256 "${shas.darwinX64}" end end on_linux do if Hardware::CPU.arm? url "https://github.com/tiann/hapi/releases/download/v#{version}/hapi-linux-arm64.tar.gz" sha256 "${shas.linuxArm64}" else url "https://github.com/tiann/hapi/releases/download/v#{version}/hapi-linux-x64.tar.gz" sha256 "${shas.linuxX64}" end end def install bin.install "hapi" end test do assert_match version.to_s, shell_output("#{bin}/hapi --version") end end `; } function printUsage(): void { console.log(`Usage: bun run scripts/update-homebrew-formula.ts --version [--push] Options: --version Version to update to (required) --push Clone tap repo, commit and push changes --help Show this help message Examples: # Generate formula locally bun run scripts/update-homebrew-formula.ts --version 0.1.0 # Push to tap repository bun run scripts/update-homebrew-formula.ts --version 0.1.0 --push `); } async function main(): Promise { const args = process.argv.slice(2); if (args.includes('--help') || args.includes('-h')) { printUsage(); process.exit(0); } const versionIdx = args.indexOf('--version'); if (versionIdx === -1 || !args[versionIdx + 1]) { console.error('Error: --version is required\n'); printUsage(); process.exit(1); } const version = args[versionIdx + 1]; const shouldPush = args.includes('--push'); const tapRepo = process.env.HOMEBREW_TAP_REPO || 'https://github.com/tiann/homebrew-tap.git'; const checksumsPath = join(projectRoot, 'release-artifacts', 'checksums.txt'); if (!existsSync(checksumsPath)) { console.error(`Error: Checksums file not found: ${checksumsPath}`); console.error('This file is generated by the CI workflow during release.'); process.exit(1); } console.log(`Generating Homebrew formula for v${version}...\n`); // Parse checksums const shas = parseChecksums(checksumsPath); console.log('SHA256 checksums:'); console.log(` darwin-arm64: ${shas.darwinArm64}`); console.log(` darwin-x64: ${shas.darwinX64}`); console.log(` linux-arm64: ${shas.linuxArm64}`); console.log(` linux-x64: ${shas.linuxX64}\n`); // Generate formula content const formulaContent = generateFormula(version, shas); if (!shouldPush) { // Just output the formula locally const localFormulaDir = join(projectRoot, 'release-artifacts', 'Formula'); mkdirSync(localFormulaDir, { recursive: true }); const localFormulaPath = join(localFormulaDir, 'hapi.rb'); writeFileSync(localFormulaPath, formulaContent); console.log(`Formula generated: ${localFormulaPath}\n`); console.log('To push to the tap repository, run with --push flag.'); console.log(`Or manually copy to your homebrew-tap repo's Formula/ directory.`); return; } // Clone and push to tap repository const githubToken = process.env.GITHUB_TOKEN; let cloneUrl = tapRepo; // Use token-authenticated URL in CI if (githubToken && tapRepo.includes('github.com')) { cloneUrl = tapRepo.replace('https://github.com/', `https://x-access-token:${githubToken}@github.com/`); } console.log(`Cloning ${tapRepo}...`); const tempDir = join(tmpdir(), `homebrew-tap-${Date.now()}`); mkdirSync(tempDir, { recursive: true }); try { execSync(`git clone --depth 1 "${cloneUrl}" "${tempDir}"`, { stdio: 'pipe' }); // Ensure Formula directory exists const formulaDir = join(tempDir, 'Formula'); mkdirSync(formulaDir, { recursive: true }); // Write formula const formulaPath = join(formulaDir, 'hapi.rb'); writeFileSync(formulaPath, formulaContent); console.log(`Updated: ${formulaPath}`); // Configure git user for CI if (githubToken) { execSync('git config user.name "github-actions[bot]"', { cwd: tempDir, stdio: 'pipe' }); execSync('git config user.email "github-actions[bot]@users.noreply.github.com"', { cwd: tempDir, stdio: 'pipe' }); } // Commit and push execSync('git add Formula/hapi.rb', { cwd: tempDir, stdio: 'pipe' }); try { execSync(`git commit -m "Update hapi to v${version}"`, { cwd: tempDir, stdio: 'pipe' }); execSync('git push origin main', { cwd: tempDir, stdio: 'pipe' }); console.log(`\nSuccessfully pushed hapi v${version} to homebrew-tap`); } catch { console.log('\nNo changes to commit (formula already up to date)'); } console.log('\nUsers can now install via:'); console.log(' brew install tiann/tap/hapi'); } finally { // Cleanup rmSync(tempDir, { recursive: true, force: true }); } } main().catch((error) => { console.error('Error:', error.message || error); process.exit(1); });