mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
feat: add npx/bunx binary distribution support
Adds infrastructure for distributing Hapi via npm as a scoped package
(@twsxtd/hapi) with platform-specific optional dependencies. This enables
installation via `npx @twsxtd/hapi` or `bunx @twsxtd/hapi`.
- Add bin/hapi.js wrapper script for platform detection and binary selection
- Create npm/ directory structure for platform-specific packages (darwin-{arm64,x64},
linux-{arm64,x64}, win32-x64)
- Add scripts/prepare-npm-packages.ts to generate platform-specific package.json files
and copy binaries to npm packages
- Add scripts/publish-npm.ts for automated publishing of platform packages
- Update package.json with new scoped name, bin entry, optionalDependencies, and
prepack/publish scripts
- Update .gitignore to ignore generated npm packages and build outputs
- Update .npmignore to only include bin/hapi.js wrapper, excluding source and build files
This commit is contained in:
@@ -11,10 +11,10 @@
|
||||
},
|
||||
},
|
||||
"cli": {
|
||||
"name": "hapi",
|
||||
"name": "@twsxtd/hapi",
|
||||
"version": "0.12.0-1",
|
||||
"bin": {
|
||||
"hapi": "./src/index.ts",
|
||||
"hapi": "bin/hapi.js",
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.22.0",
|
||||
@@ -49,6 +49,13 @@
|
||||
"typescript": "^5",
|
||||
"vitest": "^3.2.4",
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@twsxtd/hapi-darwin-arm64": "0.0.0-placeholder",
|
||||
"@twsxtd/hapi-darwin-x64": "0.0.0-placeholder",
|
||||
"@twsxtd/hapi-linux-arm64": "0.0.0-placeholder",
|
||||
"@twsxtd/hapi-linux-x64": "0.0.0-placeholder",
|
||||
"@twsxtd/hapi-win32-x64": "0.0.0-placeholder",
|
||||
},
|
||||
},
|
||||
"server": {
|
||||
"name": "hapi-server",
|
||||
@@ -588,6 +595,8 @@
|
||||
|
||||
"@tsconfig/node16": ["@tsconfig/node16@1.0.4", "", {}, "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA=="],
|
||||
|
||||
"@twsxtd/hapi": ["@twsxtd/hapi@workspace:cli"],
|
||||
|
||||
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
|
||||
|
||||
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
|
||||
@@ -1062,8 +1071,6 @@
|
||||
|
||||
"grammy": ["grammy@1.38.4", "", { "dependencies": { "@grammyjs/types": "3.22.2", "abort-controller": "^3.0.0", "debug": "^4.4.3", "node-fetch": "^2.7.0" } }, "sha512-z07Kin3HgRwMdy40KUs+c9fmNBvGlSxGwcqY8NAH0a8KULGFYEMQaFAo3ge0V5tvmgr02Jgubkf54KjHLAMCbw=="],
|
||||
|
||||
"hapi": ["hapi@workspace:cli"],
|
||||
|
||||
"hapi-server": ["hapi-server@workspace:server"],
|
||||
|
||||
"hapi-web": ["hapi-web@workspace:web"],
|
||||
|
||||
@@ -10,6 +10,14 @@ oclif.manifest.json
|
||||
# Unpacked binaries (keep archives)
|
||||
/tools/unpacked/
|
||||
|
||||
# Build outputs
|
||||
/dist-exe/
|
||||
|
||||
# Generated npm platform packages (created by prepare-npm-packages.ts)
|
||||
/npm/*/package.json
|
||||
/npm/*/bin/hapi
|
||||
/npm/*/bin/hapi.exe
|
||||
|
||||
|
||||
|
||||
pnpm-lock.yaml
|
||||
|
||||
+25
-5
@@ -1,6 +1,26 @@
|
||||
# Exclude unpacked binaries from npm package
|
||||
tools/unpacked/
|
||||
# npm package only needs bin/hapi.js (defined in package.json "files" field)
|
||||
# This file is kept for documentation purposes
|
||||
|
||||
# Keep these for npm package
|
||||
# tools/archives/
|
||||
# tools/licenses/
|
||||
# Source code (not needed - binary distribution)
|
||||
src/
|
||||
scripts/
|
||||
tools/
|
||||
demo-project/
|
||||
|
||||
# Build outputs
|
||||
dist-exe/
|
||||
npm/
|
||||
|
||||
# Config files
|
||||
*.md
|
||||
!README.md
|
||||
.github/
|
||||
.release-it.*
|
||||
bunfig.toml
|
||||
tsconfig.json
|
||||
.env*
|
||||
|
||||
# Test files
|
||||
**/*.test.ts
|
||||
**/__fixtures__/
|
||||
**/__tests__/
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { execFileSync } = require('child_process');
|
||||
const path = require('path');
|
||||
|
||||
const platform = process.platform;
|
||||
const arch = process.arch;
|
||||
const pkgName = `@twsxtd/hapi-${platform}-${arch}`;
|
||||
|
||||
function getBinaryPath() {
|
||||
try {
|
||||
// Try to find the platform-specific package
|
||||
const pkgPath = require.resolve(`${pkgName}/package.json`);
|
||||
const binName = platform === 'win32' ? 'hapi.exe' : 'hapi';
|
||||
return path.join(path.dirname(pkgPath), 'bin', binName);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const binPath = getBinaryPath();
|
||||
|
||||
if (!binPath) {
|
||||
console.error(`Unsupported platform: ${platform}-${arch}`);
|
||||
console.error('');
|
||||
console.error('Supported platforms:');
|
||||
console.error(' - darwin-arm64 (macOS Apple Silicon)');
|
||||
console.error(' - darwin-x64 (macOS Intel)');
|
||||
console.error(' - linux-arm64');
|
||||
console.error(' - linux-x64');
|
||||
console.error(' - win32-x64');
|
||||
console.error('');
|
||||
console.error('You can download the binary manually from:');
|
||||
console.error(' https://github.com/anthropics/hapi/releases');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
execFileSync(binPath, process.argv.slice(2), { stdio: 'inherit' });
|
||||
} catch (e) {
|
||||
// If the binary execution fails, exit with the same code
|
||||
if (e.status !== undefined) {
|
||||
process.exit(e.status);
|
||||
}
|
||||
// For other errors (e.g., binary not found), print and exit
|
||||
console.error(`Failed to execute ${binPath}:`, e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
+16
-2
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "hapi",
|
||||
"name": "@twsxtd/hapi",
|
||||
"version": "0.12.0-1",
|
||||
"description": "Mobile and Web client for Claude Code and Codex",
|
||||
"author": "Kirill Dubovitskiy",
|
||||
@@ -13,20 +13,34 @@
|
||||
"directory": "cli"
|
||||
},
|
||||
"bin": {
|
||||
"hapi": "./src/index.ts"
|
||||
"hapi": "bin/hapi.js"
|
||||
},
|
||||
"files": [
|
||||
"bin/hapi.js"
|
||||
],
|
||||
"imports": {
|
||||
"#embedded-assets": {
|
||||
"bun": "./src/runtime/embeddedAssets.bun.ts",
|
||||
"default": "./src/runtime/embeddedAssets.stub.ts"
|
||||
}
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@twsxtd/hapi-darwin-arm64": "0.0.0-placeholder",
|
||||
"@twsxtd/hapi-darwin-x64": "0.0.0-placeholder",
|
||||
"@twsxtd/hapi-linux-arm64": "0.0.0-placeholder",
|
||||
"@twsxtd/hapi-linux-x64": "0.0.0-placeholder",
|
||||
"@twsxtd/hapi-win32-x64": "0.0.0-placeholder"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build:exe": "bun run scripts/build-executable.ts",
|
||||
"build:exe:all": "bun run scripts/build-executable.ts --all",
|
||||
"build:exe:allinone": "bun run scripts/build-executable.ts --with-web-assets",
|
||||
"build:exe:allinone:all": "bun run scripts/build-executable.ts --with-web-assets --all",
|
||||
"prepare-npm-packages": "bun run scripts/prepare-npm-packages.ts",
|
||||
"prepack": "bun run prepare-npm-packages",
|
||||
"publish-npm": "bun run scripts/publish-npm.ts",
|
||||
"publish-npm:dry-run": "bun run scripts/publish-npm.ts --dry-run",
|
||||
"tools:unpack": "bun run scripts/unpack-tools.ts",
|
||||
"test": "tsx --env-file .env.integration-test node_modules/.bin/vitest run",
|
||||
"test:win": "vitest run",
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* Prepare npm platform packages for publishing.
|
||||
*
|
||||
* This script:
|
||||
* 1. Reads the version from cli/package.json
|
||||
* 2. Generates package.json for each platform package
|
||||
* 3. Copies binaries from dist-exe to npm package directories
|
||||
* 4. Updates optionalDependencies versions in main package.json
|
||||
*
|
||||
* Run after `bun run build:exe:all`
|
||||
*/
|
||||
|
||||
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const projectRoot = join(__dirname, '..');
|
||||
|
||||
// Platform configurations
|
||||
// Maps npm platform name to build target info
|
||||
const PLATFORMS = [
|
||||
{
|
||||
name: 'darwin-arm64',
|
||||
os: 'darwin',
|
||||
cpu: 'arm64',
|
||||
buildTarget: 'bun-darwin-arm64',
|
||||
binName: 'hapi'
|
||||
},
|
||||
{
|
||||
name: 'darwin-x64',
|
||||
os: 'darwin',
|
||||
cpu: 'x64',
|
||||
buildTarget: 'bun-darwin-x64',
|
||||
binName: 'hapi'
|
||||
},
|
||||
{
|
||||
name: 'linux-arm64',
|
||||
os: 'linux',
|
||||
cpu: 'arm64',
|
||||
buildTarget: 'bun-linux-arm64',
|
||||
binName: 'hapi'
|
||||
},
|
||||
{
|
||||
name: 'linux-x64',
|
||||
os: 'linux',
|
||||
cpu: 'x64',
|
||||
buildTarget: 'bun-linux-x64',
|
||||
binName: 'hapi'
|
||||
},
|
||||
{
|
||||
name: 'win32-x64',
|
||||
os: 'win32',
|
||||
cpu: 'x64',
|
||||
buildTarget: 'bun-windows-x64',
|
||||
binName: 'hapi.exe'
|
||||
}
|
||||
] as const;
|
||||
|
||||
interface MainPackageJson {
|
||||
version: string;
|
||||
license?: string;
|
||||
repository?: {
|
||||
type: string;
|
||||
url: string;
|
||||
};
|
||||
}
|
||||
|
||||
async function readMainPackageJson(): Promise<MainPackageJson> {
|
||||
const pkgPath = join(projectRoot, 'package.json');
|
||||
const content = await Bun.file(pkgPath).text();
|
||||
return JSON.parse(content);
|
||||
}
|
||||
|
||||
function generatePlatformPackageJson(
|
||||
platform: typeof PLATFORMS[number],
|
||||
mainPkg: MainPackageJson
|
||||
): object {
|
||||
return {
|
||||
name: `@twsxtd/hapi-${platform.name}`,
|
||||
version: mainPkg.version,
|
||||
description: `hapi binary for ${platform.os} ${platform.cpu}`,
|
||||
os: [platform.os],
|
||||
cpu: [platform.cpu],
|
||||
bin: {
|
||||
hapi: `bin/${platform.binName}`
|
||||
},
|
||||
files: [`bin/${platform.binName}`],
|
||||
license: mainPkg.license ?? 'MIT',
|
||||
repository: mainPkg.repository
|
||||
};
|
||||
}
|
||||
|
||||
async function preparePlatform(
|
||||
platform: typeof PLATFORMS[number],
|
||||
mainPkg: MainPackageJson,
|
||||
distExeDir: string,
|
||||
npmDir: string
|
||||
): Promise<void> {
|
||||
const platformDir = join(npmDir, platform.name);
|
||||
const binDir = join(platformDir, 'bin');
|
||||
|
||||
// Ensure bin directory exists
|
||||
mkdirSync(binDir, { recursive: true });
|
||||
|
||||
// Generate package.json
|
||||
const pkgJson = generatePlatformPackageJson(platform, mainPkg);
|
||||
const pkgJsonPath = join(platformDir, 'package.json');
|
||||
writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 4) + '\n');
|
||||
console.log(`Generated: ${pkgJsonPath}`);
|
||||
|
||||
// Copy binary
|
||||
const srcBin = join(distExeDir, platform.buildTarget, platform.binName);
|
||||
const destBin = join(binDir, platform.binName);
|
||||
|
||||
if (!existsSync(srcBin)) {
|
||||
console.warn(`Warning: Binary not found: ${srcBin}`);
|
||||
console.warn(` Run 'bun run build:exe:all' first to build binaries.`);
|
||||
return;
|
||||
}
|
||||
|
||||
copyFileSync(srcBin, destBin);
|
||||
console.log(`Copied: ${srcBin} -> ${destBin}`);
|
||||
}
|
||||
|
||||
function updateMainPackageOptionalDeps(version: string): void {
|
||||
const pkgPath = join(projectRoot, 'package.json');
|
||||
const content = readFileSync(pkgPath, 'utf-8');
|
||||
const pkg = JSON.parse(content);
|
||||
|
||||
// Update optionalDependencies versions
|
||||
if (!pkg.optionalDependencies) {
|
||||
pkg.optionalDependencies = {};
|
||||
}
|
||||
|
||||
for (const platform of PLATFORMS) {
|
||||
pkg.optionalDependencies[`@twsxtd/hapi-${platform.name}`] = version;
|
||||
}
|
||||
|
||||
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
||||
console.log(`Updated optionalDependencies in package.json to version ${version}`);
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
console.log('Preparing npm platform packages...\n');
|
||||
|
||||
const mainPkg = await readMainPackageJson();
|
||||
console.log(`Version: ${mainPkg.version}\n`);
|
||||
|
||||
// Update optionalDependencies in main package.json
|
||||
updateMainPackageOptionalDeps(mainPkg.version);
|
||||
|
||||
const distExeDir = join(projectRoot, 'dist-exe');
|
||||
const npmDir = join(projectRoot, 'npm');
|
||||
|
||||
let hasErrors = false;
|
||||
|
||||
for (const platform of PLATFORMS) {
|
||||
try {
|
||||
await preparePlatform(platform, mainPkg, distExeDir, npmDir);
|
||||
} catch (error) {
|
||||
console.error(`Error preparing ${platform.name}:`, error);
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\nDone!');
|
||||
|
||||
if (hasErrors) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('Fatal error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* Publish all npm packages (platform packages + main package).
|
||||
*
|
||||
* Usage:
|
||||
* bun run scripts/publish-npm.ts # Publish all packages
|
||||
* bun run scripts/publish-npm.ts --dry-run # Preview without publishing
|
||||
* bun run scripts/publish-npm.ts --skip-build # Skip building binaries
|
||||
*
|
||||
* Prerequisites:
|
||||
* - npm login (must be logged in to npm)
|
||||
* - For scoped packages: npm access must be set to public
|
||||
*/
|
||||
|
||||
import { execSync, spawnSync } from 'node:child_process';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const projectRoot = join(__dirname, '..');
|
||||
|
||||
const PLATFORMS = [
|
||||
'darwin-arm64',
|
||||
'darwin-x64',
|
||||
'linux-arm64',
|
||||
'linux-x64',
|
||||
'win32-x64'
|
||||
];
|
||||
|
||||
function parseArgs(): { dryRun: boolean; skipBuild: boolean } {
|
||||
const args = process.argv.slice(2);
|
||||
return {
|
||||
dryRun: args.includes('--dry-run'),
|
||||
skipBuild: args.includes('--skip-build')
|
||||
};
|
||||
}
|
||||
|
||||
function run(cmd: string, cwd: string = projectRoot): void {
|
||||
console.log(`\n$ ${cmd}`);
|
||||
execSync(cmd, { cwd, stdio: 'inherit' });
|
||||
}
|
||||
|
||||
function checkNpmLogin(): boolean {
|
||||
try {
|
||||
const result = spawnSync('npm', ['whoami'], { encoding: 'utf-8' });
|
||||
if (result.status === 0) {
|
||||
console.log(`Logged in as: ${result.stdout.trim()}`);
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function checkBinariesExist(): boolean {
|
||||
const binaries = [
|
||||
join(projectRoot, 'dist-exe', 'bun-darwin-arm64', 'hapi'),
|
||||
join(projectRoot, 'dist-exe', 'bun-darwin-x64', 'hapi'),
|
||||
join(projectRoot, 'dist-exe', 'bun-linux-arm64', 'hapi'),
|
||||
join(projectRoot, 'dist-exe', 'bun-linux-x64', 'hapi'),
|
||||
join(projectRoot, 'dist-exe', 'bun-windows-x64', 'hapi.exe')
|
||||
];
|
||||
|
||||
for (const bin of binaries) {
|
||||
if (!existsSync(bin)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function publishPackage(pkgDir: string, dryRun: boolean): void {
|
||||
const cmd = dryRun
|
||||
? 'npm publish --access public --dry-run'
|
||||
: 'npm publish --access public';
|
||||
run(cmd, pkgDir);
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const { dryRun, skipBuild } = parseArgs();
|
||||
|
||||
console.log('='.repeat(60));
|
||||
console.log(dryRun ? ' DRY RUN - No packages will be published' : ' PUBLISHING PACKAGES');
|
||||
console.log('='.repeat(60));
|
||||
|
||||
// Check npm login
|
||||
console.log('\n[1/5] Checking npm login...');
|
||||
if (!checkNpmLogin()) {
|
||||
console.error('Error: Not logged in to npm. Run `npm login` first.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Build binaries
|
||||
if (!skipBuild) {
|
||||
console.log('\n[2/5] Building binaries for all platforms...');
|
||||
run('bun run build:exe:all');
|
||||
} else {
|
||||
console.log('\n[2/5] Skipping build (--skip-build)');
|
||||
if (!checkBinariesExist()) {
|
||||
console.error('Error: Binaries not found. Run without --skip-build first.');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare npm packages
|
||||
console.log('\n[3/5] Preparing npm packages...');
|
||||
run('bun run prepare-npm-packages');
|
||||
|
||||
// Publish platform packages
|
||||
console.log('\n[4/5] Publishing platform packages...');
|
||||
for (const platform of PLATFORMS) {
|
||||
const pkgDir = join(projectRoot, 'npm', platform);
|
||||
console.log(`\nPublishing @twsxtd/hapi-${platform}...`);
|
||||
try {
|
||||
publishPackage(pkgDir, dryRun);
|
||||
} catch (error) {
|
||||
console.error(`Failed to publish @twsxtd/hapi-${platform}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Publish main package
|
||||
console.log('\n[5/5] Publishing main package...');
|
||||
console.log('\nPublishing @twsxtd/hapi...');
|
||||
publishPackage(projectRoot, dryRun);
|
||||
|
||||
console.log('\n' + '='.repeat(60));
|
||||
console.log(dryRun ? ' DRY RUN COMPLETE' : ' ALL PACKAGES PUBLISHED SUCCESSFULLY');
|
||||
console.log('='.repeat(60));
|
||||
|
||||
if (!dryRun) {
|
||||
const pkg = await Bun.file(join(projectRoot, 'package.json')).json();
|
||||
console.log(`\nVersion ${pkg.version} published!`);
|
||||
console.log('\nUsers can now run:');
|
||||
console.log(' npx @twsxtd/hapi');
|
||||
console.log(' bunx @twsxtd/hapi');
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('\nPublish failed:', error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user