mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
feat: add Homebrew installation support for hapi CLI
- Update CI workflow to package release artifacts with correct naming (hapi-* prefix) - Generate SHA256 checksums for all artifacts - Auto-update homebrew-tap repository on release - Add Installation section to README with Homebrew, npm, and binary options - Simplify .release-it.json config as CI now handles release process - Add update-homebrew-formula script to update formula in homebrew-tap - Add release-artifacts directory to .gitignore Users can now install via: brew install tiann/tap/hapi
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
/**
|
||||
* 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<PlatformSha> = {};
|
||||
|
||||
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 <version> [--push]
|
||||
|
||||
Options:
|
||||
--version <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<void> {
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user