scripts-orchestrator 2.10.0 → 2.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,6 @@ import path from 'path';
4
4
  import { log } from './logger.js';
5
5
  import { HealthCheck } from './health-check.js';
6
6
 
7
-
8
7
  export class ProcessManager {
9
8
  constructor() {
10
9
  this.logger = log;
@@ -13,20 +12,39 @@ export class ProcessManager {
13
12
  this.logFolder = 'scripts-orchestrator-logs'; // Default log folder
14
13
  }
15
14
 
15
+ formatDuration(ms) {
16
+ if (ms < 1000) return `${ms}ms`;
17
+ const seconds = Math.floor(ms / 1000);
18
+ const minutes = Math.floor(seconds / 60);
19
+ const remainingSeconds = seconds % 60;
20
+ if (minutes > 0) {
21
+ return `${minutes}m ${remainingSeconds}s`;
22
+ }
23
+ return `${seconds}s`;
24
+ }
25
+
16
26
  setLogFolder(logFolder) {
17
27
  this.logFolder = logFolder;
18
28
  this.logger.verbose(`Log folder set to: ${logFolder}`);
19
29
  }
20
30
 
21
31
  getLogPath(command) {
22
- const baseDir = this.logFolder ? path.resolve(this.logFolder) : process.cwd();
32
+ const baseDir = this.logFolder
33
+ ? path.resolve(this.logFolder)
34
+ : process.cwd();
23
35
  const LOGS_DIR = path.join(baseDir, 'scripts-orchestrator-logs');
24
36
  // Use only the first word of the command for the log filename
25
37
  const logName = command.split(/\s+/)[0];
26
38
  return path.join(LOGS_DIR, `${logName}.log`);
27
39
  }
28
40
 
29
- addBackgroundProcess({ command, url, startedByScript, process_tracking, kill_command }) {
41
+ addBackgroundProcess({
42
+ command,
43
+ url,
44
+ startedByScript,
45
+ process_tracking,
46
+ kill_command,
47
+ }) {
30
48
  this.logger.verbose(`Adding background process: ${command} (${url})`);
31
49
  this.backgroundProcessesDetails.push({
32
50
  command,
@@ -37,8 +55,46 @@ export class ProcessManager {
37
55
  });
38
56
  }
39
57
 
40
- async runCommand({ cmd, logFile, background = false, healthCheck = null, kill_command = null, isRetry = false, env = null }) {
41
- const baseDir = this.logFolder ? path.resolve(this.logFolder) : process.cwd();
58
+ parseGnuTimeOutput(filePath) {
59
+ try {
60
+ if (!fs.existsSync(filePath)) return null;
61
+ const text = fs.readFileSync(filePath, 'utf8');
62
+ const m = text.match(/Maximum resident set size \(kbytes\):\s*(\d+)/i);
63
+ const kbytes = m ? parseInt(m[1], 10) : null;
64
+ try {
65
+ fs.unlinkSync(filePath);
66
+ } catch {
67
+ // ignore
68
+ }
69
+ return Number.isFinite(kbytes) ? kbytes : null;
70
+ } catch {
71
+ return null;
72
+ }
73
+ }
74
+
75
+ /** Parse macOS BSD time -l output (bytes) from text; returns memory in KB or null. */
76
+ parseBsdTimeOutput(text) {
77
+ if (!text || typeof text !== 'string') return null;
78
+ const m = text.match(/(\d+)\s+maximum resident set size/i);
79
+ if (!m) return null;
80
+ const bytes = parseInt(m[1], 10);
81
+ return Number.isFinite(bytes) ? Math.round(bytes / 1024) : null;
82
+ }
83
+
84
+ async runCommand({
85
+ cmd,
86
+ logFile,
87
+ background = false,
88
+ healthCheck = null,
89
+ kill_command = null,
90
+ isRetry = false,
91
+ env = null,
92
+ reportTime = false,
93
+ reportMemory = false,
94
+ }) {
95
+ const baseDir = this.logFolder
96
+ ? path.resolve(this.logFolder)
97
+ : process.cwd();
42
98
  const LOGS_DIR = path.join(baseDir, 'scripts-orchestrator-logs');
43
99
  // Use only the first word of the command for the log filename
44
100
  const logName = cmd.split(/\s+/)[0];
@@ -54,26 +110,45 @@ export class ProcessManager {
54
110
  this.logger.verbose(`Clearing log file at ${LOG_FILE}`);
55
111
  fs.writeFileSync(LOG_FILE, ''); // Clear the log file
56
112
  } else {
57
- this.logger.verbose(`Appending to existing log file at ${LOG_FILE} (retry attempt)`);
113
+ this.logger.verbose(
114
+ `Appending to existing log file at ${LOG_FILE} (retry attempt)`,
115
+ );
58
116
  }
59
117
  } catch (error) {
60
118
  this.logger.error(`Failed to setup log file: ${error.message}`);
61
- return Promise.resolve({ success: false, output: '' });
119
+ return Promise.resolve({
120
+ success: false,
121
+ output: '',
122
+ durationMs: 0,
123
+ memoryKb: null,
124
+ });
62
125
  }
63
126
 
64
127
  return new Promise((resolve) => {
128
+ const startTime = Date.now();
129
+ let timeOutputPath = null;
65
130
  // Build command with environment variables if provided
66
131
  let fullCommand = `npm run ${cmd}`;
67
132
  if (env && Object.keys(env).length > 0) {
68
- const envStr = Object.entries(env).map(([key, value]) => `${key}=${value}`).join(' ');
133
+ const envStr = Object.entries(env)
134
+ .map(([key, value]) => `${key}=${value}`)
135
+ .join(' ');
69
136
  fullCommand = `${envStr} npm run ${cmd}`;
70
137
  }
71
-
72
- this.logger.info(`Running: ${fullCommand}`);
73
-
138
+ const useTimeWrapper =
139
+ reportMemory && !background && (process.platform === 'linux' || process.platform === 'darwin');
140
+ if (useTimeWrapper && process.platform === 'linux') {
141
+ timeOutputPath = path.join(LOGS_DIR, `.time-${logName}-${startTime}.txt`);
142
+ fullCommand = `/usr/bin/time -v -o ${JSON.stringify(timeOutputPath)} sh -c ${JSON.stringify(fullCommand)}`;
143
+ } else if (useTimeWrapper && process.platform === 'darwin') {
144
+ fullCommand = `/usr/bin/time -l sh -c ${JSON.stringify(fullCommand)}`;
145
+ }
146
+
147
+ this.logger.startTask(cmd, fullCommand);
148
+
74
149
  // Create isolated environment for each process
75
150
  const isolatedEnv = this.createIsolatedEnvironment({ command: cmd, env });
76
-
151
+
77
152
  const options = {
78
153
  shell: true,
79
154
  detached: background,
@@ -91,30 +166,41 @@ export class ProcessManager {
91
166
  const processInstance = spawn(fullCommand, [], options);
92
167
 
93
168
  processInstance.on('error', (error) => {
169
+ this.logger.stopTask(cmd);
94
170
  this.logger.error(`Failed to start process: ${error.message}`);
95
- //this.logger.verbose(`Process error details: ${JSON.stringify(error, null, 2)}`);
96
- resolve({ success: false, output: '' });
171
+ resolve({
172
+ success: false,
173
+ output: '',
174
+ durationMs: Date.now() - startTime,
175
+ memoryKb: null,
176
+ });
97
177
  });
98
178
 
99
179
  if (background) {
100
180
  const processGroupId = processInstance.pid;
101
- this.logger.verbose(`Background process spawned with PID: ${processGroupId}`);
181
+ this.logger.verbose(
182
+ `Background process spawned with PID: ${processGroupId}`,
183
+ );
102
184
 
103
185
  // Track process exit for background processes
104
186
  let processExited = false;
105
187
  let processExitCode = null;
106
-
188
+
107
189
  processInstance.on('exit', (code, signal) => {
108
190
  processExited = true;
109
191
  processExitCode = code;
110
- this.logger.verbose(`Background process ${cmd} (PID: ${processGroupId}) exited with code: ${code}, signal: ${signal}`);
192
+ this.logger.verbose(
193
+ `Background process ${cmd} (PID: ${processGroupId}) exited with code: ${code}, signal: ${signal}`,
194
+ );
111
195
  });
112
196
 
113
197
  processInstance.stdout.on('data', (data) => {
114
198
  try {
115
199
  fs.appendFileSync(LOG_FILE, data.toString());
116
200
  } catch (error) {
117
- this.logger.error(`Failed to write to log file: ${error.message}`);
201
+ this.logger.error(
202
+ `Failed to write to log file: ${error.message}`,
203
+ );
118
204
  }
119
205
  });
120
206
 
@@ -122,7 +208,9 @@ export class ProcessManager {
122
208
  try {
123
209
  fs.appendFileSync(LOG_FILE, data.toString());
124
210
  } catch (error) {
125
- this.logger.error(`Failed to write to log file: ${error.message}`);
211
+ this.logger.error(
212
+ `Failed to write to log file: ${error.message}`,
213
+ );
126
214
  }
127
215
  });
128
216
 
@@ -134,37 +222,59 @@ export class ProcessManager {
134
222
  try {
135
223
  // First check if the process has already exited with an error
136
224
  if (processExited && processExitCode !== 0) {
137
- this.logger.error(`Background process ${cmd} exited with code ${processExitCode}`);
225
+ this.logger.stopTask(cmd);
226
+ this.logger.error(
227
+ `Background process ${cmd} exited with code ${processExitCode}`,
228
+ );
138
229
  let output = '';
139
230
  try {
140
231
  output = fs.readFileSync(LOG_FILE, 'utf8');
141
232
  this.logger.verbose(`Process output: ${output}`);
142
233
  } catch (error) {
143
- this.logger.error(`Failed to read log file: ${error.message}`);
234
+ this.logger.error(
235
+ `Failed to read log file: ${error.message}`,
236
+ );
144
237
  }
145
- return { success: false, output };
238
+ return {
239
+ success: false,
240
+ output,
241
+ durationMs: Date.now() - startTime,
242
+ memoryKb: null,
243
+ };
146
244
  }
147
-
148
- this.logger.verbose(`Verifying process ${processGroupId} (attempt ${attempt}/${maxAttempts})`);
245
+
246
+ this.logger.verbose(
247
+ `Verifying process ${processGroupId} (attempt ${attempt}/${maxAttempts})`,
248
+ );
149
249
  process.kill(processGroupId, 0);
150
250
  this.logger.verbose(`Process ${processGroupId} is running`);
151
-
251
+
152
252
  // Wait a bit more to ensure the process doesn't exit immediately after verification
153
253
  await new Promise((resolve) => setTimeout(resolve, 500));
154
-
254
+
155
255
  // Check again if the process exited during our wait
156
256
  if (processExited && processExitCode !== 0) {
157
- this.logger.error(`Background process ${cmd} exited with code ${processExitCode} shortly after starting`);
257
+ this.logger.stopTask(cmd);
258
+ this.logger.error(
259
+ `Background process ${cmd} exited with code ${processExitCode} shortly after starting`,
260
+ );
158
261
  let output = '';
159
262
  try {
160
263
  output = fs.readFileSync(LOG_FILE, 'utf8');
161
264
  this.logger.verbose(`Process output: ${output}`);
162
265
  } catch (error) {
163
- this.logger.error(`Failed to read log file: ${error.message}`);
266
+ this.logger.error(
267
+ `Failed to read log file: ${error.message}`,
268
+ );
164
269
  }
165
- return { success: false, output };
270
+ return {
271
+ success: false,
272
+ output,
273
+ durationMs: Date.now() - startTime,
274
+ memoryKb: null,
275
+ };
166
276
  }
167
-
277
+
168
278
  this.backgroundProcesses.push(processGroupId);
169
279
  this.backgroundProcessesDetails.push({
170
280
  command: cmd,
@@ -174,28 +284,52 @@ export class ProcessManager {
174
284
  startedByScript: true,
175
285
  kill_command,
176
286
  });
177
-
287
+
178
288
  this.logger.verbose(`Unreferencing process ${processGroupId}`);
179
289
  processInstance.unref();
180
-
290
+
291
+ this.logger.stopTask(cmd);
181
292
  this.logger.verbose(
182
293
  `Background process started: npm run ${cmd} (PGID: ${processGroupId})`,
183
294
  );
184
- return { success: true, output: '' };
295
+ return {
296
+ success: true,
297
+ output: '',
298
+ durationMs: Date.now() - startTime,
299
+ memoryKb: null,
300
+ };
185
301
  } catch (error) {
186
302
  if (attempt === maxAttempts) {
187
- this.logger.error(`Failed to start background process: npm run ${cmd}`);
188
- this.logger.verbose(`Final verification attempt failed: ${error.message}`);
189
- return { success: false, output: '' };
303
+ this.logger.error(
304
+ `Failed to start background process: npm run ${cmd}`,
305
+ );
306
+ this.logger.verbose(
307
+ `Final verification attempt failed: ${error.message}`,
308
+ );
309
+ return {
310
+ success: false,
311
+ output: '',
312
+ durationMs: Date.now() - startTime,
313
+ memoryKb: null,
314
+ };
190
315
  }
191
- this.logger.verbose(`Verification attempt ${attempt} failed: ${error.message}`);
192
- this.logger.verbose(`Waiting ${baseDelay * Math.pow(2, attempt - 1)}ms before next attempt`);
316
+ this.logger.verbose(
317
+ `Verification attempt ${attempt} failed: ${error.message}`,
318
+ );
319
+ this.logger.verbose(
320
+ `Waiting ${baseDelay * Math.pow(2, attempt - 1)}ms before next attempt`,
321
+ );
193
322
  await new Promise((resolve) =>
194
323
  setTimeout(resolve, baseDelay * Math.pow(2, attempt - 1)),
195
324
  );
196
325
  }
197
326
  }
198
- return { success: false, output: '' };
327
+ return {
328
+ success: false,
329
+ output: '',
330
+ durationMs: Date.now() - startTime,
331
+ memoryKb: null,
332
+ };
199
333
  };
200
334
 
201
335
  verifyProcess().then(resolve);
@@ -204,7 +338,9 @@ export class ProcessManager {
204
338
  try {
205
339
  fs.appendFileSync(LOG_FILE, data.toString());
206
340
  } catch (error) {
207
- this.logger.error(`Failed to write to log file: ${error.message}`);
341
+ this.logger.error(
342
+ `Failed to write to log file: ${error.message}`,
343
+ );
208
344
  }
209
345
  });
210
346
 
@@ -212,7 +348,9 @@ export class ProcessManager {
212
348
  try {
213
349
  fs.appendFileSync(LOG_FILE, data.toString());
214
350
  } catch (error) {
215
- this.logger.error(`Failed to write to log file: ${error.message}`);
351
+ this.logger.error(
352
+ `Failed to write to log file: ${error.message}`,
353
+ );
216
354
  }
217
355
  });
218
356
 
@@ -224,20 +362,52 @@ export class ProcessManager {
224
362
  this.logger.error(`Failed to read log file: ${error.message}`);
225
363
  }
226
364
 
365
+ this.logger.stopTask(cmd);
366
+
367
+ const durationMs = Date.now() - startTime;
368
+ const durationStr = reportTime
369
+ ? ` (${this.formatDuration(durationMs)})`
370
+ : '';
371
+ let memoryKb = null;
372
+ if (reportMemory) {
373
+ if (timeOutputPath) {
374
+ memoryKb = this.parseGnuTimeOutput(timeOutputPath);
375
+ } else if (process.platform === 'darwin') {
376
+ memoryKb = this.parseBsdTimeOutput(output);
377
+ }
378
+ }
379
+
227
380
  if (code !== 0) {
228
- this.logger.error(`Failed: npm run ${cmd} (exit code: ${code})`);
381
+ this.logger.error(
382
+ `Failed: npm run ${cmd} ❌${durationStr} (exit code: ${code})`,
383
+ );
229
384
  this.logger.verbose(`Process output: ${output}`);
230
- resolve({ success: false, output });
385
+ resolve({
386
+ success: false,
387
+ output,
388
+ durationMs,
389
+ memoryKb,
390
+ });
231
391
  } else {
232
- this.logger.success(`Completed: npm run ${cmd}`);
233
- resolve({ success: true, output });
392
+ this.logger.success(`Completed: npm run ${cmd} ✅${durationStr}`);
393
+ resolve({
394
+ success: true,
395
+ output,
396
+ durationMs,
397
+ memoryKb,
398
+ });
234
399
  }
235
400
  });
236
401
  }
237
402
  } catch (error) {
403
+ this.logger.stopTask(cmd);
238
404
  this.logger.error(`Failed to spawn process: ${error.message}`);
239
- //this.logger.verbose(`Spawn error details: ${JSON.stringify(error, null, 2)}`);
240
- resolve({ success: false, output: '' });
405
+ resolve({
406
+ success: false,
407
+ output: '',
408
+ durationMs: Date.now() - startTime,
409
+ memoryKb: null,
410
+ });
241
411
  }
242
412
  });
243
413
  }
@@ -245,7 +415,7 @@ export class ProcessManager {
245
415
  createIsolatedEnvironment({ command, env = null }) {
246
416
  // Create a deep copy to avoid any reference sharing
247
417
  const baseEnv = JSON.parse(JSON.stringify(process.env));
248
-
418
+
249
419
  // Set standard environment variables
250
420
  const isolatedEnv = {
251
421
  ...baseEnv,
@@ -272,25 +442,37 @@ export class ProcessManager {
272
442
  // Remove any potentially problematic environment variables
273
443
  delete isolatedEnv.npm_lifecycle_event;
274
444
  delete isolatedEnv.npm_lifecycle_script;
275
-
445
+
276
446
  return isolatedEnv;
277
447
  }
278
448
 
279
449
  async cleanup() {
280
450
  try {
281
451
  this.logger.info('\nCleaning up background processes...');
282
-
452
+
283
453
  // Debug: Log the number of processes we're tracking
284
- this.logger.info(`- Found ${this.backgroundProcessesDetails.length} background processes to clean up`);
285
-
454
+ this.logger.info(
455
+ `- Found ${this.backgroundProcessesDetails.length} background processes to clean up`,
456
+ );
457
+
286
458
  // Debug: Log each process details
287
- this.backgroundProcessesDetails.forEach(({ command, pgid, url, startedByScript, kill_command }, index) => {
288
- this.logger.verbose(`- Process ${index + 1}: command=${command}, pgid=${pgid}, url=${url}, startedByScript=${startedByScript}, kill_command=${kill_command}`);
289
- });
290
-
459
+ this.backgroundProcessesDetails.forEach(
460
+ ({ command, pgid, url, startedByScript, kill_command }, index) => {
461
+ this.logger.verbose(
462
+ `- Process ${index + 1}: command=${command}, pgid=${pgid}, url=${url}, startedByScript=${startedByScript}, kill_command=${kill_command}`,
463
+ );
464
+ },
465
+ );
466
+
291
467
  const killPromises = this.backgroundProcessesDetails.map(
292
468
  async ({ command, pgid, url, startedByScript, kill_command }) => {
293
- await this.cleanupProcess({ command, pgid, url, startedByScript, kill_command });
469
+ await this.cleanupProcess({
470
+ command,
471
+ pgid,
472
+ url,
473
+ startedByScript,
474
+ kill_command,
475
+ });
294
476
  },
295
477
  );
296
478
 
@@ -304,33 +486,43 @@ export class ProcessManager {
304
486
 
305
487
  async cleanupCommand(commandName) {
306
488
  this.logger.info(`\nCleaning up processes for command: ${commandName}`);
307
-
489
+
308
490
  // Find processes for this specific command
309
491
  const commandProcesses = this.backgroundProcessesDetails.filter(
310
- ({ command }) => command === commandName
492
+ ({ command }) => command === commandName,
311
493
  );
312
-
494
+
313
495
  if (commandProcesses.length === 0) {
314
- this.logger.verbose(`- No background processes found for command: ${commandName}`);
496
+ this.logger.verbose(
497
+ `- No background processes found for command: ${commandName}`,
498
+ );
315
499
  return;
316
500
  }
317
-
318
- this.logger.verbose(`- Found ${commandProcesses.length} background processes for command: ${commandName}`);
319
-
501
+
502
+ this.logger.verbose(
503
+ `- Found ${commandProcesses.length} background processes for command: ${commandName}`,
504
+ );
505
+
320
506
  const killPromises = commandProcesses.map(
321
507
  async ({ command, pgid, url, startedByScript, kill_command }) => {
322
- await this.cleanupProcess({ command, pgid, url, startedByScript, kill_command });
323
- }
508
+ await this.cleanupProcess({
509
+ command,
510
+ pgid,
511
+ url,
512
+ startedByScript,
513
+ kill_command,
514
+ });
515
+ },
324
516
  );
325
517
 
326
518
  await Promise.allSettled(killPromises);
327
-
519
+
328
520
  // Remove the cleaned up processes from our tracking arrays
329
- this.backgroundProcesses = this.backgroundProcesses.filter(pgid =>
330
- !commandProcesses.some(proc => proc.pgid === pgid)
521
+ this.backgroundProcesses = this.backgroundProcesses.filter(
522
+ (pgid) => !commandProcesses.some((proc) => proc.pgid === pgid),
331
523
  );
332
524
  this.backgroundProcessesDetails = this.backgroundProcessesDetails.filter(
333
- ({ command }) => command !== commandName
525
+ ({ command }) => command !== commandName,
334
526
  );
335
527
  }
336
528
 
@@ -342,24 +534,40 @@ export class ProcessManager {
342
534
  return;
343
535
  }
344
536
 
345
- this.logger.verbose(`- Processing cleanup for ${command} (kill_command: ${kill_command})`);
537
+ this.logger.verbose(
538
+ `- Processing cleanup for ${command} (kill_command: ${kill_command})`,
539
+ );
346
540
 
347
541
  // Try custom kill command first if specified
348
542
  if (kill_command) {
349
543
  try {
350
- this.logger.verbose(`- Using custom kill command: npm run ${kill_command}`);
351
- const result = await this.runCommand({ cmd: kill_command, logFile: null, background: false });
544
+ this.logger.verbose(
545
+ `- Using custom kill command: npm run ${kill_command}`,
546
+ );
547
+ const result = await this.runCommand({
548
+ cmd: kill_command,
549
+ logFile: null,
550
+ background: false,
551
+ });
352
552
  if (result.success) {
353
- this.logger.verbose(`- Successfully killed ${command} using custom command`);
553
+ this.logger.verbose(
554
+ `- Successfully killed ${command} using custom command`,
555
+ );
354
556
  return;
355
557
  } else {
356
- this.logger.verbose('- Custom kill command failed, falling back to process signals');
558
+ this.logger.verbose(
559
+ '- Custom kill command failed, falling back to process signals',
560
+ );
357
561
  }
358
562
  } catch (error) {
359
- this.logger.verbose(`- Custom kill command error: ${error.message}, falling back`);
563
+ this.logger.verbose(
564
+ `- Custom kill command error: ${error.message}, falling back`,
565
+ );
360
566
  }
361
567
  } else {
362
- this.logger.verbose(`- No kill_command specified for ${command}, using process signals`);
568
+ this.logger.verbose(
569
+ `- No kill_command specified for ${command}, using process signals`,
570
+ );
363
571
  }
364
572
 
365
573
  try {
@@ -375,27 +583,36 @@ export class ProcessManager {
375
583
 
376
584
  // Cross-platform process termination
377
585
  const isWindows = process.platform === 'win32';
378
-
586
+
379
587
  if (isWindows) {
380
588
  // Windows: use taskkill to terminate process tree
381
589
  try {
382
- const killProcess = spawn('taskkill', ['/F', '/T', '/PID', pgid.toString()]);
590
+ const killProcess = spawn('taskkill', [
591
+ '/F',
592
+ '/T',
593
+ '/PID',
594
+ pgid.toString(),
595
+ ]);
383
596
  await new Promise((resolve) => {
384
597
  killProcess.on('close', resolve);
385
598
  });
386
- this.logger.verbose(`- Terminated background process: ${command} (PID: ${pgid})`);
599
+ this.logger.verbose(
600
+ `- Terminated background process: ${command} (PID: ${pgid})`,
601
+ );
387
602
  return;
388
603
  } catch (killError) {
389
- this.logger.verbose(`- Failed to use taskkill, falling back to process.kill: ${killError.message}`);
604
+ this.logger.verbose(
605
+ `- Failed to use taskkill, falling back to process.kill: ${killError.message}`,
606
+ );
390
607
  }
391
608
  }
392
-
609
+
393
610
  // Unix/Linux/macOS or Windows fallback: Try SIGTERM first
394
611
  process.kill(pgid, 'SIGTERM');
395
612
 
396
613
  await new Promise((resolve, reject) => {
397
614
  let timeout, checkInterval;
398
-
615
+
399
616
  timeout = setTimeout(() => {
400
617
  if (checkInterval) clearInterval(checkInterval);
401
618
  reject(new Error('Process termination timeout'));
@@ -415,26 +632,31 @@ export class ProcessManager {
415
632
  `- Terminated background process: ${command} (PGID: ${pgid})`,
416
633
  );
417
634
  } catch (error) {
418
- this.logger.verbose(`- Failed to terminate process group: ${error.message}`);
635
+ this.logger.verbose(
636
+ `- Failed to terminate process group: ${error.message}`,
637
+ );
419
638
  }
420
639
 
421
640
  // Check if the URL is still responding after termination attempt
422
641
  if (url) {
423
642
  try {
424
643
  const urlObj = new URL(url);
425
- const port = urlObj.port || (urlObj.protocol === 'https:' ? '443' : '80');
426
-
644
+ const port =
645
+ urlObj.port || (urlObj.protocol === 'https:' ? '443' : '80');
646
+
427
647
  // Use shared HTTP utility for cross-platform compatibility
428
648
  const urlResult = await HealthCheck.makeHttpRequest(url, 2000);
429
-
649
+
430
650
  if (urlResult.success && urlResult.statusCode === 200) {
431
- this.logger.verbose(`- URL ${url} is still responding after termination, finding process on port ${port}`);
432
-
651
+ this.logger.verbose(
652
+ `- URL ${url} is still responding after termination, finding process on port ${port}`,
653
+ );
654
+
433
655
  // Find and kill process using the port - cross-platform approach
434
656
  try {
435
657
  const isWindows = process.platform === 'win32';
436
658
  let findPortCmd, findPortArgs;
437
-
659
+
438
660
  if (isWindows) {
439
661
  // Windows: use netstat
440
662
  findPortCmd = 'netstat';
@@ -444,7 +666,7 @@ export class ProcessManager {
444
666
  findPortCmd = 'lsof';
445
667
  findPortArgs = ['-i', `:${port}`, '-t'];
446
668
  }
447
-
669
+
448
670
  const findProcess = spawn(findPortCmd, findPortArgs);
449
671
  const result = await new Promise((resolve) => {
450
672
  let output = '';
@@ -458,12 +680,15 @@ export class ProcessManager {
458
680
 
459
681
  if (result.code === 0 && result.output.trim()) {
460
682
  let pids = [];
461
-
683
+
462
684
  if (isWindows) {
463
685
  // Parse netstat output to find PIDs for the specific port
464
686
  const lines = result.output.split('\n');
465
687
  for (const line of lines) {
466
- if (line.includes(`:${port} `) && line.includes('LISTENING')) {
688
+ if (
689
+ line.includes(`:${port} `) &&
690
+ line.includes('LISTENING')
691
+ ) {
467
692
  const parts = line.trim().split(/\s+/);
468
693
  const pid = parts[parts.length - 1];
469
694
  if (pid && !isNaN(pid)) {
@@ -475,7 +700,7 @@ export class ProcessManager {
475
700
  // lsof output is already just PIDs
476
701
  pids = result.output.trim().split('\n');
477
702
  }
478
-
703
+
479
704
  for (const pid of pids) {
480
705
  try {
481
706
  if (isWindows) {
@@ -488,16 +713,22 @@ export class ProcessManager {
488
713
  // Unix/Linux/macOS: use process.kill
489
714
  process.kill(parseInt(pid), 'SIGKILL');
490
715
  }
491
- this.logger.verbose(`- Killed process (PID: ${pid}) using port ${port}`);
716
+ this.logger.verbose(
717
+ `- Killed process (PID: ${pid}) using port ${port}`,
718
+ );
492
719
  } catch (killError) {
493
720
  if (killError.code !== 'ESRCH') {
494
- this.logger.error(`- Failed to kill process (PID: ${pid}): ${killError.message}`);
721
+ this.logger.error(
722
+ `- Failed to kill process (PID: ${pid}): ${killError.message}`,
723
+ );
495
724
  }
496
725
  }
497
726
  }
498
727
  }
499
728
  } catch (portError) {
500
- this.logger.error(`- Failed to find process using port ${port}: ${portError.message}`);
729
+ this.logger.error(
730
+ `- Failed to find process using port ${port}: ${portError.message}`,
731
+ );
501
732
  }
502
733
  }
503
734
  } catch (error) {
@@ -508,10 +739,15 @@ export class ProcessManager {
508
739
  // Final attempt to kill the process group
509
740
  try {
510
741
  const isWindows = process.platform === 'win32';
511
-
742
+
512
743
  if (isWindows) {
513
744
  // Windows: force kill with taskkill
514
- const killProcess = spawn('taskkill', ['/F', '/T', '/PID', pgid.toString()]);
745
+ const killProcess = spawn('taskkill', [
746
+ '/F',
747
+ '/T',
748
+ '/PID',
749
+ pgid.toString(),
750
+ ]);
515
751
  await new Promise((resolve) => {
516
752
  killProcess.on('close', resolve);
517
753
  });
@@ -528,4 +764,4 @@ export class ProcessManager {
528
764
  }
529
765
 
530
766
  // For backward compatibility
531
- export const processManager = new ProcessManager();
767
+ export const processManager = new ProcessManager();