@@ -26,6 +26,7 @@ export interface TowerStartOptions {
2626
2727export interface TowerStopOptions {
2828 port ?: number ;
29+ forceKillAllChildProcesses ?: boolean ;
2930}
3031
3132/**
@@ -214,8 +215,9 @@ export async function towerStart(options: TowerStartOptions = {}): Promise<void>
214215 */
215216export async function towerStop ( options : TowerStopOptions = { } ) : Promise < void > {
216217 const port = options . port || DEFAULT_TOWER_PORT ;
218+ const forceKill = options . forceKillAllChildProcesses || false ;
217219
218- logger . header ( 'Stopping Tower' ) ;
220+ logger . header ( forceKill ? 'Force-Killing Tower and All Child Processes' : 'Stopping Tower' ) ;
219221
220222 const pids = getProcessesOnPort ( port ) ;
221223
@@ -224,6 +226,56 @@ export async function towerStop(options: TowerStopOptions = {}): Promise<void> {
224226 return ;
225227 }
226228
229+ if ( forceKill ) {
230+ // Shellper processes are spawned DETACHED from Tower — they intentionally
231+ // survive Tower restarts. So pgrep -P won't find them. We need to:
232+ // 1. Find all shellper-main processes via pgrep -f
233+ // 2. Find all their children (claude, bash, etc.)
234+ // 3. Kill the Tower daemon itself
235+ const { execSync } = await import ( 'node:child_process' ) ;
236+ const allPids = new Set < number > ( ) ;
237+
238+ // Recursive function to collect entire subtree via pgrep -P
239+ function collectDescendants ( pid : number ) : void {
240+ if ( allPids . has ( pid ) ) return ;
241+ allPids . add ( pid ) ;
242+ try {
243+ const output = execSync ( `pgrep -P ${ pid } ` , { encoding : 'utf-8' } ) . trim ( ) ;
244+ for ( const line of output . split ( '\n' ) ) {
245+ const childPid = parseInt ( line , 10 ) ;
246+ if ( ! isNaN ( childPid ) ) collectDescendants ( childPid ) ;
247+ }
248+ } catch { /* no children */ }
249+ }
250+
251+ // Collect Tower daemon PIDs
252+ for ( const pid of pids ) {
253+ collectDescendants ( pid ) ;
254+ }
255+
256+ // Collect ALL shellper processes and their descendants (claude, bash, etc.)
257+ try {
258+ const shellperOutput = execSync ( 'pgrep -f shellper-main' , { encoding : 'utf-8' } ) . trim ( ) ;
259+ for ( const line of shellperOutput . split ( '\n' ) ) {
260+ const pid = parseInt ( line , 10 ) ;
261+ if ( ! isNaN ( pid ) ) collectDescendants ( pid ) ;
262+ }
263+ } catch { /* no shellper processes */ }
264+
265+ // Kill leaves first (reverse order: deepest descendants → root)
266+ const orderedPids = [ ...allPids ] . reverse ( ) ;
267+ let killed = 0 ;
268+ for ( const pid of orderedPids ) {
269+ try {
270+ process . kill ( pid , 'SIGKILL' ) ;
271+ killed ++ ;
272+ } catch { /* already dead */ }
273+ }
274+
275+ logger . success ( `Force-killed ${ killed } process(es) (tower + ${ orderedPids . length - pids . length } shellper/children)` ) ;
276+ return ;
277+ }
278+
227279 let stopped = 0 ;
228280 for ( const pid of pids ) {
229281 try {
0 commit comments