Skip to content

Commit 03264f8

Browse files
jasonczcclaude
andcommitted
feat(cli): add haqi worker refresh
SIGTERMs the worker in $HAPI_HOME/worker.lock, waits for it to exit, then posts to the hub's /api/cloud/start-local-worker to respawn a fresh bun process that re-reads the CLI source. Saves the kill + curl dance when iterating on CLI code (e.g. picking up new RPC handlers without restarting the whole dev stack). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent fada315 commit 03264f8

1 file changed

Lines changed: 90 additions & 0 deletions

File tree

cli/src/commands/worker.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import chalk from 'chalk'
2+
import { existsSync, readFileSync } from 'node:fs'
3+
import { join } from 'node:path'
24
import { configuration } from '@/configuration'
35
import { startWorker } from '@/worker/workerStart'
46
import { readWorkerConfig, clearWorkerConfig } from '@/worker/workerConfig'
@@ -19,6 +21,82 @@ function parseWorkerStartArgs(args: string[]): { token?: string; hubUrl?: string
1921
return { token, hubUrl }
2022
}
2123

24+
function readWorkerLockPid(): number | null {
25+
const lockFile = join(configuration.happyHomeDir, 'worker.lock')
26+
if (!existsSync(lockFile)) return null
27+
try {
28+
const raw = readFileSync(lockFile, 'utf-8').trim()
29+
const pid = Number.parseInt(raw, 10)
30+
return Number.isFinite(pid) && pid > 0 ? pid : null
31+
} catch {
32+
return null
33+
}
34+
}
35+
36+
function isPidAlive(pid: number): boolean {
37+
try {
38+
process.kill(pid, 0)
39+
return true
40+
} catch {
41+
return false
42+
}
43+
}
44+
45+
async function refreshWorker(): Promise<void> {
46+
const pid = readWorkerLockPid()
47+
if (pid && isPidAlive(pid)) {
48+
console.log(chalk.gray(`Stopping current worker (pid ${pid})…`))
49+
try {
50+
process.kill(pid, 'SIGTERM')
51+
} catch (err) {
52+
console.error(chalk.red(`Failed to signal pid ${pid}: ${err instanceof Error ? err.message : String(err)}`))
53+
}
54+
// Wait for it to exit so its worker.lock is released before the hub spawns a replacement
55+
for (let i = 0; i < 40; i++) {
56+
if (!isPidAlive(pid)) break
57+
await new Promise(r => setTimeout(r, 100))
58+
}
59+
if (isPidAlive(pid)) {
60+
console.log(chalk.yellow(`Worker pid ${pid} still alive after 4s — hub will evict it via worker.lock`))
61+
}
62+
} else {
63+
console.log(chalk.gray('No live worker detected from worker.lock — asking hub to spawn one…'))
64+
}
65+
66+
if (!configuration.cliApiToken) {
67+
throw new Error('CLI_API_TOKEN is not configured. Run `hapi auth login` first.')
68+
}
69+
70+
const url = `${configuration.apiUrl.replace(/\/$/, '')}/api/cloud/start-local-worker`
71+
const res = await fetch(url, {
72+
method: 'POST',
73+
headers: {
74+
authorization: `Bearer ${configuration.cliApiToken}`,
75+
'content-type': 'application/json',
76+
},
77+
body: '{}',
78+
})
79+
80+
if (!res.ok) {
81+
const text = await res.text().catch(() => '')
82+
throw new Error(`Hub rejected request (${res.status}): ${text.slice(0, 400)}`)
83+
}
84+
85+
const result = await res.json() as {
86+
started?: boolean
87+
pid?: number
88+
alreadyRunning?: boolean
89+
evictedPid?: number
90+
startedAt?: number
91+
}
92+
93+
const parts: string[] = []
94+
if (result.pid) parts.push(`pid ${result.pid}`)
95+
if (result.evictedPid) parts.push(`evicted ${result.evictedPid}`)
96+
if (result.alreadyRunning) parts.push('already running')
97+
console.log(chalk.green(`Worker refreshed${parts.length ? ` — ${parts.join(', ')}` : ''}`))
98+
}
99+
22100
export const workerCommand: CommandDefinition = {
23101
name: 'worker',
24102
requiresRuntimeAssets: true,
@@ -60,6 +138,16 @@ export const workerCommand: CommandDefinition = {
60138
return
61139
}
62140

141+
if (subcommand === 'refresh') {
142+
try {
143+
await refreshWorker()
144+
} catch (err) {
145+
console.error(chalk.red('Error:'), err instanceof Error ? err.message : 'Unknown error')
146+
process.exit(1)
147+
}
148+
return
149+
}
150+
63151
console.log(`
64152
${chalk.bold('haqi worker')} - Self-hosted worker management
65153
@@ -69,6 +157,8 @@ ${chalk.bold('Usage:')}
69157
haqi worker stop Show instructions to stop the running worker
70158
haqi worker status Display saved worker configuration
71159
haqi worker reset Clear saved worker configuration
160+
haqi worker refresh Kill the local worker + ask the hub to respawn it
161+
(picks up updated CLI source without a full restart)
72162
73163
${chalk.bold('Notes:')}
74164
- The worker runs in the ${chalk.yellow('foreground')} (not detached).

0 commit comments

Comments
 (0)