mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
449 lines
15 KiB
TypeScript
Executable File
449 lines
15 KiB
TypeScript
Executable File
#!/usr/bin/env bun
|
|
|
|
/**
|
|
* CLI entry point for hapi command
|
|
*
|
|
* Simple argument parsing without any CLI framework dependencies
|
|
*/
|
|
|
|
|
|
import chalk from 'chalk'
|
|
import type { StartOptions } from '@/claude/runClaude'
|
|
import { logger } from './ui/logger'
|
|
import { authAndSetupMachineIfNeeded } from './ui/auth'
|
|
import packageJson from '../package.json'
|
|
import { isBunCompiled } from './projectPath'
|
|
import { z } from 'zod'
|
|
import { startDaemon } from './daemon/run'
|
|
import { checkIfDaemonRunningAndCleanupStaleState, isDaemonRunningCurrentlyInstalledHappyVersion, stopDaemon } from './daemon/controlClient'
|
|
import { getLatestDaemonLog } from './ui/logger'
|
|
import { killRunawayHappyProcesses } from './daemon/doctor'
|
|
import { install } from './daemon/install'
|
|
import { uninstall } from './daemon/uninstall'
|
|
import { runDoctorCommand } from './ui/doctor'
|
|
import { listDaemonSessions, stopDaemonSession } from './daemon/controlClient'
|
|
import { handleAuthCommand } from './commands/auth'
|
|
import { handleConnectCommand } from './commands/connect'
|
|
import { spawnHappyCLI } from './utils/spawnHappyCLI'
|
|
import { execFileSync } from 'node:child_process'
|
|
import { initializeToken } from './ui/tokenInit'
|
|
import { ensureRuntimeAssets } from './runtime/assets'
|
|
import { runHappyMcpStdioBridge } from './codex/happyMcpStdioBridge'
|
|
import { withBunRuntimeEnv } from './utils/bunRuntime'
|
|
import { getCliArgs } from './utils/cliArgs'
|
|
|
|
|
|
(async () => {
|
|
const args = getCliArgs()
|
|
|
|
if (args.includes('-v') || args.includes('--version')) {
|
|
console.log(`hapi version: ${packageJson.version}`)
|
|
process.exit(0)
|
|
}
|
|
|
|
// Check if first argument is a subcommand
|
|
const subcommand = args[0]
|
|
|
|
if (isBunCompiled()) {
|
|
process.env.DEV = 'false'
|
|
}
|
|
|
|
if (subcommand === 'mcp') {
|
|
await runHappyMcpStdioBridge(args.slice(1))
|
|
return
|
|
}
|
|
|
|
if (subcommand === 'server') {
|
|
try {
|
|
await import('../../server/src/index')
|
|
} catch (error) {
|
|
console.error(chalk.red('Error:'), error instanceof Error ? error.message : 'Unknown error')
|
|
if (process.env.DEBUG) {
|
|
console.error(error)
|
|
}
|
|
process.exit(1)
|
|
}
|
|
return
|
|
}
|
|
|
|
if (subcommand === 'hook-forwarder') {
|
|
const { runSessionHookForwarder } = await import('@/claude/utils/sessionHookForwarder')
|
|
await runSessionHookForwarder(args.slice(1))
|
|
return
|
|
}
|
|
|
|
await ensureRuntimeAssets()
|
|
|
|
logger.debug('Starting hapi CLI with args: ', process.argv)
|
|
|
|
if (subcommand === 'doctor') {
|
|
// Check for clean subcommand
|
|
if (args[1] === 'clean') {
|
|
const result = await killRunawayHappyProcesses()
|
|
console.log(`Cleaned up ${result.killed} runaway processes`)
|
|
if (result.errors.length > 0) {
|
|
console.log('Errors:', result.errors)
|
|
}
|
|
process.exit(0)
|
|
}
|
|
await runDoctorCommand();
|
|
return;
|
|
} else if (subcommand === 'auth') {
|
|
// Handle auth subcommands
|
|
try {
|
|
await handleAuthCommand(args.slice(1));
|
|
} catch (error) {
|
|
console.error(chalk.red('Error:'), error instanceof Error ? error.message : 'Unknown error')
|
|
if (process.env.DEBUG) {
|
|
console.error(error)
|
|
}
|
|
process.exit(1)
|
|
}
|
|
return;
|
|
} else if (subcommand === 'connect') {
|
|
// Handle connect subcommands
|
|
try {
|
|
await handleConnectCommand(args.slice(1));
|
|
} catch (error) {
|
|
console.error(chalk.red('Error:'), error instanceof Error ? error.message : 'Unknown error')
|
|
if (process.env.DEBUG) {
|
|
console.error(error)
|
|
}
|
|
process.exit(1)
|
|
}
|
|
return;
|
|
} else if (subcommand === 'codex') {
|
|
// Handle codex command
|
|
try {
|
|
const { runCodex } = await import('@/codex/runCodex');
|
|
|
|
// Parse known arguments and collect unknown ones for passthrough
|
|
const options: { startedBy?: 'daemon' | 'terminal'; codexArgs?: string[] } = {};
|
|
const unknownArgs: string[] = [];
|
|
for (let i = 1; i < args.length; i++) {
|
|
const arg = args[i];
|
|
if (arg === '--started-by') {
|
|
options.startedBy = args[++i] as 'daemon' | 'terminal';
|
|
} else {
|
|
unknownArgs.push(arg);
|
|
}
|
|
}
|
|
if (unknownArgs.length > 0) {
|
|
options.codexArgs = unknownArgs;
|
|
}
|
|
|
|
await initializeToken();
|
|
await authAndSetupMachineIfNeeded();
|
|
await runCodex(options);
|
|
// Do not force exit here; allow instrumentation to show lingering handles
|
|
} catch (error) {
|
|
console.error(chalk.red('Error:'), error instanceof Error ? error.message : 'Unknown error')
|
|
if (process.env.DEBUG) {
|
|
console.error(error)
|
|
}
|
|
process.exit(1)
|
|
}
|
|
return;
|
|
} else if (subcommand === 'gemini') {
|
|
// Handle gemini command
|
|
try {
|
|
await import('./agent/runners/gemini');
|
|
const { runAgentSession } = await import('./agent/runners/runAgentSession');
|
|
|
|
let startedBy: 'daemon' | 'terminal' | undefined = undefined;
|
|
for (let i = 1; i < args.length; i++) {
|
|
if (args[i] === '--started-by') {
|
|
startedBy = args[++i] as 'daemon' | 'terminal';
|
|
}
|
|
}
|
|
|
|
await initializeToken();
|
|
await authAndSetupMachineIfNeeded();
|
|
await runAgentSession({ agentType: 'gemini', startedBy });
|
|
} catch (error) {
|
|
console.error(chalk.red('Error:'), error instanceof Error ? error.message : 'Unknown error')
|
|
if (process.env.DEBUG) {
|
|
console.error(error)
|
|
}
|
|
process.exit(1)
|
|
}
|
|
return;
|
|
} else if (subcommand === 'logout') {
|
|
// Keep for backward compatibility - redirect to auth logout
|
|
console.log(chalk.yellow('Note: "hapi logout" is deprecated. Use "hapi auth logout" instead.\n'));
|
|
try {
|
|
await handleAuthCommand(['logout']);
|
|
} catch (error) {
|
|
console.error(chalk.red('Error:'), error instanceof Error ? error.message : 'Unknown error')
|
|
if (process.env.DEBUG) {
|
|
console.error(error)
|
|
}
|
|
process.exit(1)
|
|
}
|
|
return;
|
|
} else if (subcommand === 'notify') {
|
|
// Handle notification command
|
|
console.error(chalk.red('The `hapi notify` command is not available in direct-connect mode.'))
|
|
console.error(chalk.gray('Use Telegram notifications from hapi-server instead.'))
|
|
process.exit(1)
|
|
return;
|
|
} else if (subcommand === 'daemon') {
|
|
// Show daemon management help
|
|
const daemonSubcommand = args[1]
|
|
|
|
if (daemonSubcommand === 'list') {
|
|
try {
|
|
const sessions = await listDaemonSessions()
|
|
|
|
if (sessions.length === 0) {
|
|
console.log('No active sessions this daemon is aware of (they might have been started by a previous version of the daemon)')
|
|
} else {
|
|
console.log('Active sessions:')
|
|
console.log(JSON.stringify(sessions, null, 2))
|
|
}
|
|
} catch (error) {
|
|
console.log('No daemon running')
|
|
}
|
|
return
|
|
|
|
} else if (daemonSubcommand === 'stop-session') {
|
|
const sessionId = args[2]
|
|
if (!sessionId) {
|
|
console.error('Session ID required')
|
|
process.exit(1)
|
|
}
|
|
|
|
try {
|
|
const success = await stopDaemonSession(sessionId)
|
|
console.log(success ? 'Session stopped' : 'Failed to stop session')
|
|
} catch (error) {
|
|
console.log('No daemon running')
|
|
}
|
|
return
|
|
|
|
} else if (daemonSubcommand === 'start') {
|
|
// Spawn detached daemon process
|
|
const child = spawnHappyCLI(['daemon', 'start-sync'], {
|
|
detached: true,
|
|
stdio: 'ignore',
|
|
env: process.env
|
|
});
|
|
child.unref();
|
|
|
|
// Wait for daemon to write state file (up to 5 seconds)
|
|
let started = false;
|
|
for (let i = 0; i < 50; i++) {
|
|
if (await checkIfDaemonRunningAndCleanupStaleState()) {
|
|
started = true;
|
|
break;
|
|
}
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
}
|
|
|
|
if (started) {
|
|
console.log('Daemon started successfully');
|
|
} else {
|
|
console.error('Failed to start daemon');
|
|
process.exit(1);
|
|
}
|
|
process.exit(0);
|
|
} else if (daemonSubcommand === 'start-sync') {
|
|
await initializeToken();
|
|
await startDaemon()
|
|
process.exit(0)
|
|
} else if (daemonSubcommand === 'stop') {
|
|
await stopDaemon()
|
|
process.exit(0)
|
|
} else if (daemonSubcommand === 'status') {
|
|
// Show daemon-specific doctor output
|
|
await runDoctorCommand('daemon')
|
|
process.exit(0)
|
|
} else if (daemonSubcommand === 'logs') {
|
|
// Simply print the path to the latest daemon log file
|
|
const latest = await getLatestDaemonLog()
|
|
if (!latest) {
|
|
console.log('No daemon logs found')
|
|
} else {
|
|
console.log(latest.path)
|
|
}
|
|
process.exit(0)
|
|
} else if (daemonSubcommand === 'install') {
|
|
try {
|
|
await install()
|
|
} catch (error) {
|
|
console.error(chalk.red('Error:'), error instanceof Error ? error.message : 'Unknown error')
|
|
process.exit(1)
|
|
}
|
|
} else if (daemonSubcommand === 'uninstall') {
|
|
try {
|
|
await uninstall()
|
|
} catch (error) {
|
|
console.error(chalk.red('Error:'), error instanceof Error ? error.message : 'Unknown error')
|
|
process.exit(1)
|
|
}
|
|
} else {
|
|
console.log(`
|
|
${chalk.bold('hapi daemon')} - Daemon management
|
|
|
|
${chalk.bold('Usage:')}
|
|
hapi daemon start Start the daemon (detached)
|
|
hapi daemon stop Stop the daemon (sessions stay alive)
|
|
hapi daemon status Show daemon status
|
|
hapi daemon list List active sessions
|
|
|
|
If you want to kill all hapi related processes run
|
|
${chalk.cyan('hapi doctor clean')}
|
|
|
|
${chalk.bold('Note:')} The daemon runs in the background and manages Claude sessions.
|
|
|
|
${chalk.bold('To clean up runaway processes:')} Use ${chalk.cyan('hapi doctor clean')}
|
|
`)
|
|
}
|
|
return;
|
|
} else {
|
|
|
|
// If the first argument is claude, remove it
|
|
if (args.length > 0 && args[0] === 'claude') {
|
|
args.shift()
|
|
}
|
|
|
|
// Parse command line arguments for main command
|
|
const options: StartOptions = {}
|
|
let showHelp = false
|
|
const unknownArgs: string[] = [] // Collect unknown args to pass through to claude
|
|
|
|
for (let i = 0; i < args.length; i++) {
|
|
const arg = args[i]
|
|
|
|
if (arg === '-h' || arg === '--help') {
|
|
showHelp = true
|
|
// Also pass through to claude
|
|
unknownArgs.push(arg)
|
|
} else if (arg === '--hapi-starting-mode') {
|
|
options.startingMode = z.enum(['local', 'remote']).parse(args[++i])
|
|
} else if (arg === '--yolo') {
|
|
// Shortcut for --dangerously-skip-permissions
|
|
unknownArgs.push('--dangerously-skip-permissions')
|
|
} else if (arg === '--started-by') {
|
|
options.startedBy = args[++i] as 'daemon' | 'terminal'
|
|
} else {
|
|
// Pass unknown arguments through to claude
|
|
unknownArgs.push(arg)
|
|
// Check if this arg expects a value (simplified check for common patterns)
|
|
if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
|
|
unknownArgs.push(args[++i])
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add unknown args to claudeArgs
|
|
if (unknownArgs.length > 0) {
|
|
options.claudeArgs = [...(options.claudeArgs || []), ...unknownArgs]
|
|
}
|
|
|
|
// Show help
|
|
if (showHelp) {
|
|
console.log(`
|
|
${chalk.bold('hapi')} - Claude Code On the Go
|
|
|
|
${chalk.bold('Usage:')}
|
|
hapi [options] Start Claude with Telegram control (direct-connect)
|
|
hapi auth Manage authentication
|
|
hapi codex Start Codex mode
|
|
hapi gemini Start Gemini ACP mode
|
|
hapi mcp Start MCP stdio bridge
|
|
hapi connect (not available in direct-connect mode)
|
|
hapi notify (not available in direct-connect mode)
|
|
hapi server Start the API + web server
|
|
hapi daemon Manage background service that allows
|
|
to spawn new sessions away from your computer
|
|
hapi doctor System diagnostics & troubleshooting
|
|
|
|
${chalk.bold('Examples:')}
|
|
hapi Start session (will prompt for token if not set)
|
|
hapi auth login Configure CLI_API_TOKEN interactively
|
|
hapi --yolo Start with bypassing permissions
|
|
hapi sugar for --dangerously-skip-permissions
|
|
hapi auth status Show direct-connect status
|
|
hapi doctor Run diagnostics
|
|
|
|
${chalk.bold('hapi supports ALL Claude options!')}
|
|
Use any claude flag with hapi as you would with claude. Our favorite:
|
|
|
|
hapi --resume
|
|
|
|
${chalk.gray('─'.repeat(60))}
|
|
${chalk.bold.cyan('Claude Code Options (from `claude --help`):')}
|
|
`)
|
|
|
|
// Run claude --help and display its output
|
|
try {
|
|
const claudeHelp = execFileSync(
|
|
'claude',
|
|
['--help'],
|
|
{ encoding: 'utf8', env: withBunRuntimeEnv(), shell: process.platform === 'win32' }
|
|
)
|
|
console.log(claudeHelp)
|
|
} catch (e) {
|
|
console.log(chalk.yellow('Could not retrieve claude help. Make sure claude is installed.'))
|
|
}
|
|
|
|
process.exit(0)
|
|
}
|
|
|
|
// Normal flow - auth and machine setup
|
|
await initializeToken();
|
|
await authAndSetupMachineIfNeeded();
|
|
|
|
// Always auto-start daemon for simplicity
|
|
logger.debug('Ensuring hapi background service is running & matches our version...');
|
|
|
|
if (!(await isDaemonRunningCurrentlyInstalledHappyVersion())) {
|
|
logger.debug('Starting hapi background service...');
|
|
|
|
// Use the built binary to spawn daemon
|
|
const daemonProcess = spawnHappyCLI(['daemon', 'start-sync'], {
|
|
detached: true,
|
|
stdio: 'ignore',
|
|
env: process.env
|
|
})
|
|
daemonProcess.unref();
|
|
|
|
// Give daemon a moment to write PID & port file
|
|
await new Promise(resolve => setTimeout(resolve, 200));
|
|
}
|
|
|
|
// Start the CLI
|
|
try {
|
|
const { runClaude } = await import('@/claude/runClaude');
|
|
await runClaude(options);
|
|
} catch (error) {
|
|
// Categorize errors for better user experience
|
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
const messageLower = message.toLowerCase();
|
|
const axiosCode = (error as any)?.code;
|
|
const httpStatus = (error as any)?.response?.status;
|
|
|
|
if (axiosCode === 'ECONNREFUSED' || axiosCode === 'ETIMEDOUT' || axiosCode === 'ENOTFOUND' ||
|
|
messageLower.includes('econnrefused') || messageLower.includes('etimedout') ||
|
|
messageLower.includes('enotfound') || messageLower.includes('network error')) {
|
|
const { configuration } = await import('@/configuration');
|
|
console.error(chalk.yellow('Unable to connect to HAPI server'));
|
|
console.error(chalk.gray(` Server URL: ${configuration.serverUrl}`));
|
|
console.error(chalk.gray(' Please check your network connection or server status'));
|
|
} else if (httpStatus === 401 || httpStatus === 403 ||
|
|
messageLower.includes('unauthorized') || messageLower.includes('forbidden')) {
|
|
console.error(chalk.red('Authentication error:'), message);
|
|
console.error(chalk.gray(' Run: hapi auth login'));
|
|
} else {
|
|
console.error(chalk.red('Error:'), message);
|
|
}
|
|
|
|
if (process.env.DEBUG) {
|
|
console.error(error)
|
|
}
|
|
process.exit(1)
|
|
}
|
|
}
|
|
})();
|