11import chalk from 'chalk'
2+ import { existsSync , readFileSync } from 'node:fs'
3+ import { join } from 'node:path'
24import { configuration } from '@/configuration'
35import { startWorker } from '@/worker/workerStart'
46import { 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+
22100export 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