scripts-orchestrator 2.10.0 → 2.12.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.
@@ -1,4 +1,5 @@
1
1
  import { log } from './logger.js';
2
+ import chalk from 'chalk';
2
3
 
3
4
  export class HealthCheck {
4
5
  constructor() {
@@ -17,17 +18,17 @@ export class HealthCheck {
17
18
  const urlObj = new URL(url);
18
19
  const isHttps = urlObj.protocol === 'https:';
19
20
  const httpModule = isHttps ? await import('https') : await import('http');
20
-
21
+
21
22
  return await new Promise((resolve) => {
22
23
  const req = httpModule.default.get(url, (res) => {
23
24
  resolve({ statusCode: res.statusCode, success: true });
24
25
  res.destroy(); // Close the response stream
25
26
  });
26
-
27
+
27
28
  req.on('error', (error) => {
28
29
  resolve({ statusCode: null, success: false, error: error.message });
29
30
  });
30
-
31
+
31
32
  req.setTimeout(timeout, () => {
32
33
  req.destroy();
33
34
  resolve({ statusCode: null, success: false, error: 'Timeout' });
@@ -38,30 +39,45 @@ export class HealthCheck {
38
39
  }
39
40
  }
40
41
 
41
- async waitForUrl({url, maxAttempts = 20, interval = 2000, silent=false}) {
42
- !silent && this.logger.info(`Waiting for ${url} to be available...`);
42
+ async waitForUrl({ url, maxAttempts = 20, interval = 2000, silent = false }) {
43
+ if (!silent) {
44
+ this.logger.startEphemeral(
45
+ `wait_${url}`,
46
+ chalk.yellow(`[INFO] ⏳ Waiting for ${url} to be available...`),
47
+ );
48
+ }
43
49
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
44
50
  try {
45
51
  const result = await HealthCheck.makeHttpRequest(url, 5000);
46
52
 
47
53
  if (result.success && result.statusCode === 200) {
48
- !silent && this.logger.success(`${url} is available`);
54
+ if (!silent) {
55
+ this.logger.stopEphemeral(`wait_${url}`, `✅ ${url} is available`);
56
+ }
49
57
  return true;
50
58
  }
51
59
  } catch (error) {
52
- !silent && this.logger.verbose(`Attempt ${attempt}/${maxAttempts} failed: ${error.message}`);
60
+ if (!silent) {
61
+ // You could potentially log attempts via verbose if needed, but not necessary.
62
+ }
53
63
  }
54
64
 
55
65
  if (attempt < maxAttempts) {
56
66
  await new Promise((resolve) => setTimeout(resolve, interval));
57
67
  }
58
68
  }
59
-
60
- !silent && this.logger.error(`Failed to connect to ${url} after ${maxAttempts} attempts`);
61
-
69
+
70
+ if (!silent) {
71
+ this.logger.stopEphemeral(
72
+ `wait_${url}`,
73
+ `❌ Failed to connect to ${url} after ${maxAttempts} attempts`,
74
+ true,
75
+ );
76
+ }
77
+
62
78
  return false;
63
79
  }
64
80
  }
65
81
 
66
82
  // For backward compatibility
67
- export const healthCheck = new HealthCheck();
83
+ export const healthCheck = new HealthCheck();
package/lib/logger.js CHANGED
@@ -30,9 +30,131 @@ class Logger {
30
30
  this.logFolder = argv.logFolder || 'scripts-orchestrator-logs';
31
31
  this.logFile = null;
32
32
  this.logStream = null;
33
+
34
+ // For TTY dynamic output
35
+ this.isTTY = process.stdout.isTTY;
36
+ this.activeTasks = new Map();
37
+ this.linesRendered = 0;
38
+
33
39
  this.initializeLogFile();
34
40
  }
35
41
 
42
+ // --- Dynamic Output Handling ---
43
+
44
+ clearActiveTasks() {
45
+ if (!this.isTTY || this.linesRendered === 0) return;
46
+ // Move cursor up and clear lines
47
+ for (let i = 0; i < this.linesRendered; i++) {
48
+ process.stdout.write('\x1b[1A\x1b[2K'); // Up one line, clear entire line
49
+ }
50
+ this.linesRendered = 0;
51
+ }
52
+
53
+ renderActiveTasks() {
54
+ if (!this.isTTY) return;
55
+ if (this.activeTasks.size === 0) return;
56
+
57
+ // Render active tasks with a spinner or prefix
58
+ for (const [, text] of this.activeTasks) {
59
+ process.stdout.write(`${text}\n`);
60
+ this.linesRendered++;
61
+ }
62
+ }
63
+
64
+ startTask(id, text) {
65
+ if (this.isTTY) {
66
+ this.clearActiveTasks();
67
+ this.activeTasks.set(id, chalk.cyan(`[INFO] ⏳ Running: ${text}`));
68
+ this.renderActiveTasks();
69
+ } else {
70
+ this.info(`Running: ${text}`);
71
+ }
72
+ }
73
+
74
+ updateTask(id, text) {
75
+ if (this.isTTY && this.activeTasks.has(id)) {
76
+ this.clearActiveTasks();
77
+ this.activeTasks.set(id, chalk.cyan(`[INFO] ⏳ Running: ${text}`));
78
+ this.renderActiveTasks();
79
+ }
80
+ }
81
+
82
+ stopTask(id) {
83
+ if (this.isTTY && this.activeTasks.has(id)) {
84
+ this.clearActiveTasks();
85
+ this.activeTasks.delete(id);
86
+ this.renderActiveTasks();
87
+ }
88
+ }
89
+
90
+ // --- Wrapper for output methods ---
91
+
92
+ printMessage(logFn) {
93
+ if (this.isTTY) {
94
+ this.clearActiveTasks();
95
+ logFn();
96
+ this.renderActiveTasks();
97
+ } else {
98
+ logFn();
99
+ }
100
+ }
101
+
102
+ startEphemeral(id, message) {
103
+ if (this.isTTY) {
104
+ this.clearActiveTasks();
105
+ this.activeTasks.set(id, message);
106
+ this.renderActiveTasks();
107
+ } else {
108
+ this.printMessage(() => console.log(message));
109
+ }
110
+ }
111
+
112
+ stopEphemeral(id, finalMessage = '', isError = false) {
113
+ if (this.isTTY && this.activeTasks.has(id)) {
114
+ this.clearActiveTasks();
115
+ this.activeTasks.delete(id);
116
+ this.renderActiveTasks();
117
+ }
118
+
119
+ if (finalMessage) {
120
+ if (isError) {
121
+ this.printMessage(() =>
122
+ console.error(chalk.red(`[ERROR] ${finalMessage}`)),
123
+ );
124
+ } else {
125
+ this.printMessage(() =>
126
+ console.log(chalk.green(`[SUCCESS] ${finalMessage}`)),
127
+ );
128
+ }
129
+ }
130
+ }
131
+
132
+ stopPhase(phaseName, success, durationStr) {
133
+ if (success) {
134
+ this.printMessage(() =>
135
+ console.log(
136
+ chalk.green(
137
+ `[SUCCESS] ✅ Phase "${phaseName}" completed successfully ${durationStr}`,
138
+ ),
139
+ ),
140
+ );
141
+ // Add a blank line after phase logs for readability
142
+ this.printMessage(() => console.log(''));
143
+ this.writeToFile('');
144
+ } else {
145
+ this.printMessage(() =>
146
+ console.error(
147
+ chalk.red(
148
+ `[ERROR] ❌ Phase "${phaseName}" completed with failures ${durationStr}`,
149
+ ),
150
+ ),
151
+ );
152
+ // Add a blank line after phase logs for readability
153
+ this.printMessage(() => console.log(''));
154
+ this.writeToFile('');
155
+ }
156
+ }
157
+
36
158
  initializeLogFile() {
37
159
  try {
38
160
  // Create log directory if it doesn't exist
@@ -41,12 +163,18 @@ class Logger {
41
163
  }
42
164
 
43
165
  // Create main log file with timestamp
44
- const timestamp = new Date().toISOString().replace(/:/g, '-').replace(/\..+/, '');
45
- this.logFile = path.join(this.logFolder, `orchestrator-main-${timestamp}.log`);
46
-
166
+ const timestamp = new Date()
167
+ .toISOString()
168
+ .replace(/:/g, '-')
169
+ .replace(/\..+/, '');
170
+ this.logFile = path.join(
171
+ this.logFolder,
172
+ `orchestrator-main-${timestamp}.log`,
173
+ );
174
+
47
175
  // Create write stream
48
176
  this.logStream = fs.createWriteStream(this.logFile, { flags: 'a' });
49
-
177
+
50
178
  // Handle stream errors
51
179
  this.logStream.on('error', (err) => {
52
180
  console.error(`Error writing to log file: ${err.message}`);
@@ -60,7 +188,9 @@ class Logger {
60
188
  });
61
189
 
62
190
  // Write initial log entry
63
- this.writeToFile(`[START] Orchestrator started at ${new Date().toISOString()}\n`);
191
+ this.writeToFile(
192
+ `[START] Orchestrator started at ${new Date().toISOString()}\n`,
193
+ );
64
194
  } catch (error) {
65
195
  console.error(`Failed to initialize log file: ${error.message}`);
66
196
  }
@@ -74,7 +204,7 @@ class Logger {
74
204
 
75
205
  // Update log folder
76
206
  this.logFolder = newLogFolder;
77
-
207
+
78
208
  // Reinitialize with new folder
79
209
  this.initializeLogFile();
80
210
  }
@@ -89,28 +219,28 @@ class Logger {
89
219
  }
90
220
 
91
221
  info(message) {
92
- console.log(chalk.blue(`[INFO] ${message}`));
222
+ this.printMessage(() => console.log(chalk.blue(`[INFO] ${message}`)));
93
223
  this.writeToFile(`[INFO] ${message}`);
94
224
  }
95
225
 
96
226
  success(message) {
97
- console.log(chalk.green(`[SUCCESS] ${message}`));
227
+ this.printMessage(() => console.log(chalk.green(`[SUCCESS] ${message}`)));
98
228
  this.writeToFile(`[SUCCESS] ${message}`);
99
229
  }
100
230
 
101
231
  error(message) {
102
- console.error(chalk.red(`[ERROR] ${message}`));
232
+ this.printMessage(() => console.error(chalk.red(`[ERROR] ${message}`)));
103
233
  this.writeToFile(`[ERROR] ${message}`);
104
234
  }
105
235
 
106
236
  warn(message) {
107
- console.warn(chalk.yellow(`[WARN] ${message}`));
237
+ this.printMessage(() => console.warn(chalk.yellow(`[WARN] ${message}`)));
108
238
  this.writeToFile(`[WARN] ${message}`);
109
239
  }
110
240
 
111
241
  verbose(message) {
112
242
  if (this.isVerbose) {
113
- console.log(chalk.gray(`[VERBOSE] ${message}`));
243
+ this.printMessage(() => console.log(chalk.gray(`[VERBOSE] ${message}`)));
114
244
  this.writeToFile(`[VERBOSE] ${message}`);
115
245
  }
116
246
  }
@@ -120,4 +250,4 @@ class Logger {
120
250
  const logger = new Logger();
121
251
 
122
252
  // Export both the class and the instance
123
- export { Logger, logger as log };
253
+ export { Logger, logger as log };
@@ -2,9 +2,17 @@ import { processManager } from './process-manager.js';
2
2
  import { healthCheck } from './health-check.js';
3
3
  import { log } from './logger.js';
4
4
  import { GitCache } from './git-cache.js';
5
+ import chalk from 'chalk';
5
6
 
6
7
  export class Orchestrator {
7
- constructor(config, startPhase = null, logFolder = null, phases = null, sequential = false, force = false) {
8
+ constructor(
9
+ config,
10
+ startPhase = null,
11
+ logFolder = null,
12
+ phases = null,
13
+ sequential = false,
14
+ force = false,
15
+ ) {
8
16
  this.config = config;
9
17
  this.startPhase = startPhase;
10
18
  this.logFolder = logFolder;
@@ -16,14 +24,15 @@ export class Orchestrator {
16
24
  this.logger = log;
17
25
  this.failedCommands = [];
18
26
  this.skippedCommands = [];
27
+ this.skipReasons = new Map(); // Track why commands were skipped
19
28
  this.commandTimings = new Map();
20
29
  this.gitCache = new GitCache(logFolder);
21
-
30
+
22
31
  // Set the log folder in process manager
23
32
  if (logFolder) {
24
33
  this.processManager.setLogFolder(logFolder);
25
34
  }
26
-
35
+
27
36
  // Flatten commands for easier tracking
28
37
  this.allCommands = this.flattenCommands(config);
29
38
  }
@@ -33,11 +42,11 @@ export class Orchestrator {
33
42
  if (Array.isArray(config)) {
34
43
  return config;
35
44
  }
36
-
45
+
37
46
  if (config.phases) {
38
- return config.phases.flatMap(phase => phase.parallel || []);
47
+ return config.phases.flatMap((phase) => phase.parallel || []);
39
48
  }
40
-
49
+
41
50
  return [];
42
51
  }
43
52
 
@@ -86,6 +95,7 @@ export class Orchestrator {
86
95
  if (status === 'disabled') {
87
96
  this.logger.warn(`Skipping: npm run ${command} (status: disabled)`);
88
97
  this.skippedCommands.push(command);
98
+ this.skipReasons.set(command, 'disabled');
89
99
  this.commandTimings.set(command, Date.now() - startTime);
90
100
  visited.delete(command);
91
101
  return true;
@@ -93,10 +103,23 @@ export class Orchestrator {
93
103
 
94
104
  const checkUrl = health_check?.url;
95
105
  if (checkUrl) {
96
- this.logger.info(`Checking if ${checkUrl} is already available...`);
97
- const urlAvailable = await this.healthCheck.waitForUrl({url: checkUrl, maxAttempts: 1, silent:true});
98
- if (urlAvailable) {
99
- this.logger.verbose(`${checkUrl} is already available. Skipping ${command} start.`);
106
+ this.logger.startEphemeral(
107
+ `check_${checkUrl}`,
108
+ chalk.blue(`[INFO] ⏳ Checking if ${checkUrl} is already available...`),
109
+ );
110
+ const urlAvailable = await this.healthCheck.waitForUrl({
111
+ url: checkUrl,
112
+ maxAttempts: 1,
113
+ silent: true,
114
+ });
115
+
116
+ if (!urlAvailable) {
117
+ this.logger.stopEphemeral(`check_${checkUrl}`);
118
+ } else {
119
+ this.logger.stopEphemeral(
120
+ `check_${checkUrl}`,
121
+ `✅ ${checkUrl} is already available. Skipping ${command} start.`,
122
+ );
100
123
  this.processManager.addBackgroundProcess({
101
124
  command,
102
125
  url: checkUrl,
@@ -116,23 +139,21 @@ export class Orchestrator {
116
139
  if (!dependencySuccess) {
117
140
  this.logger.error(`Skipping ${command} due to failed dependency`);
118
141
  this.skippedCommands.push(command);
142
+ this.skipReasons.set(command, 'failed_dependency');
119
143
  this.commandTimings.set(command, Date.now() - startTime);
120
144
  visited.delete(command);
121
145
  return false;
122
146
  }
123
147
 
124
148
  if (dependency.health_check?.url) {
125
- this.logger.info(`Waiting for ${dependency.health_check.url} to be available...`);
126
149
  const urlAvailable = await this.healthCheck.waitForUrl({
127
150
  url: dependency.health_check.url,
128
151
  maxAttempts: dependency.health_check?.max_attempts || 20,
129
152
  interval: dependency.health_check?.interval || 2000,
130
153
  });
131
154
  if (!urlAvailable) {
132
- this.logger.error(
133
- `URL ${dependency.health_check.url} is not available after maximum attempts`,
134
- );
135
155
  this.skippedCommands.push(command);
156
+ this.skipReasons.set(command, 'failed_dependency');
136
157
  this.commandTimings.set(command, Date.now() - startTime);
137
158
  visited.delete(command);
138
159
  return false;
@@ -141,7 +162,9 @@ export class Orchestrator {
141
162
  this.logger.verbose(`Waiting ${dependency.wait}ms`);
142
163
  await new Promise((resolve) => {
143
164
  setTimeout(() => {
144
- this.logger.verbose(`Resolving after a wait of ${dependency.wait}ms`);
165
+ this.logger.verbose(
166
+ `Resolving after a wait of ${dependency.wait}ms`,
167
+ );
145
168
  resolve(true);
146
169
  }, dependency.wait);
147
170
  });
@@ -153,10 +176,12 @@ export class Orchestrator {
153
176
  let result = false;
154
177
  let commandOutput = '';
155
178
  let commandFailed = false;
156
-
179
+
157
180
  for (let attempt = 1; attempt <= attempts; attempt++) {
158
181
  if (attempt > 1) {
159
- this.logger.warn(`Retrying ${command} (attempt ${attempt}/${attempts})`);
182
+ this.logger.warn(
183
+ `Retrying ${command} (attempt ${attempt}/${attempts})`,
184
+ );
160
185
  await new Promise((resolve) => setTimeout(resolve, 1000));
161
186
  }
162
187
 
@@ -174,7 +199,9 @@ export class Orchestrator {
174
199
 
175
200
  if (result) {
176
201
  // Remove from failed commands if it was there
177
- this.failedCommands = this.failedCommands.filter(cmd => cmd !== command);
202
+ this.failedCommands = this.failedCommands.filter(
203
+ (cmd) => cmd !== command,
204
+ );
178
205
  commandFailed = false;
179
206
  break;
180
207
  } else if (attempt < attempts) {
@@ -185,7 +212,9 @@ export class Orchestrator {
185
212
  commandFailed = true;
186
213
  break;
187
214
  }
188
- this.logger.error(`Attempt ${attempt}/${attempts} failed for ${command}`);
215
+ this.logger.error(
216
+ `Attempt ${attempt}/${attempts} failed for ${command}`,
217
+ );
189
218
  commandFailed = true;
190
219
  } else {
191
220
  commandFailed = true;
@@ -194,14 +223,18 @@ export class Orchestrator {
194
223
 
195
224
  if (commandFailed) {
196
225
  this.failedCommands.push(command);
197
-
226
+
198
227
  // Cleanup any background processes for this failed command
199
228
  if (background) {
200
- this.logger.warn(`Command ${command} failed after all attempts. Cleaning up background processes.`);
229
+ this.logger.warn(
230
+ `Command ${command} failed after all attempts. Cleaning up background processes.`,
231
+ );
201
232
  try {
202
233
  await this.processManager.cleanupCommand(command);
203
234
  } catch (cleanupError) {
204
- this.logger.error(`Failed to cleanup processes for ${command}: ${cleanupError.message}`);
235
+ this.logger.error(
236
+ `Failed to cleanup processes for ${command}: ${cleanupError.message}`,
237
+ );
205
238
  }
206
239
  }
207
240
  }
@@ -212,34 +245,32 @@ export class Orchestrator {
212
245
  }
213
246
 
214
247
  summarizeResults() {
215
- this.logger.info('\nCommand Summary:');
216
248
  let hasFailures = false;
217
-
249
+
250
+ // Check if any command failed or was skipped due to failure
218
251
  this.allCommands.forEach(({ command }) => {
219
- const duration = this.commandTimings.get(command);
220
- const durationStr = duration ? ` (${this.formatDuration(duration)})` : '';
221
-
222
252
  if (this.failedCommands.includes(command)) {
223
253
  hasFailures = true;
224
- // Get the actual log path from process manager
225
- const logPath = this.processManager.getLogPath(command);
226
- this.logger.error(`- ${command}: ❌${durationStr} (See ${logPath})`);
227
254
  } else if (this.skippedCommands.includes(command)) {
228
- hasFailures = true;
229
- this.logger.warn(`- ${command}: ⚠️${durationStr} (Skipped due to failed dependency)`);
230
- } else {
231
- this.logger.success(`- ${command}: ✅${durationStr}`);
255
+ const skipReason = this.skipReasons.get(command);
256
+ if (
257
+ skipReason === 'failed_dependency' ||
258
+ skipReason === 'after_phase_failure'
259
+ ) {
260
+ hasFailures = true;
261
+ }
232
262
  }
233
263
  });
234
264
 
235
265
  if (hasFailures) {
236
- this.logger.error('\n❌ Some commands failed or were skipped. See details above.');
266
+ this.logger.error('\n❌ Some commands failed or were skipped.');
237
267
  } else {
238
268
  this.logger.success('\n🎉 All commands executed successfully!');
239
269
  }
240
270
  }
241
271
 
242
272
  async run() {
273
+ this.startTime = Date.now();
243
274
  try {
244
275
  // Check if we should skip execution based on git state (unless forced)
245
276
  if (!this.force) {
@@ -250,7 +281,9 @@ export class Orchestrator {
250
281
  process.exit(0);
251
282
  }
252
283
  } else {
253
- this.logger.info('⚡ Force execution enabled, skipping git cache check');
284
+ this.logger.info(
285
+ '⚡ Force execution enabled, skipping git cache check',
286
+ );
254
287
  }
255
288
 
256
289
  let hasFailures = false;
@@ -276,14 +309,14 @@ export class Orchestrator {
276
309
  this.executeCommand(commandConfig),
277
310
  );
278
311
  const results = await Promise.all(tasks);
279
- hasFailures = results.some(result => !result);
312
+ hasFailures = results.some((result) => !result);
280
313
  }
281
314
  } else if (this.config.phases) {
282
315
  // New: Run phases sequentially, commands within phases in parallel or sequential based on flag
283
316
  if (this.sequential) {
284
317
  this.logger.info('🔄 Running in sequential mode');
285
318
  }
286
-
319
+
287
320
  for (const phase of this.config.phases) {
288
321
  // Check if we should start from this phase
289
322
  if (this.startPhase && !startPhaseFound) {
@@ -294,6 +327,7 @@ export class Orchestrator {
294
327
  // Mark all commands in previous phases as skipped
295
328
  phase.parallel.forEach(({ command }) => {
296
329
  this.skippedCommands.push(command);
330
+ this.skipReasons.set(command, 'before_start_phase');
297
331
  this.commandTimings.set(command, 0);
298
332
  });
299
333
  continue;
@@ -301,11 +335,18 @@ export class Orchestrator {
301
335
  }
302
336
 
303
337
  // Check if this is an optional phase that should be skipped
304
- if (phase.optional === true && this.phases && !this.phases.includes(phase.name)) {
305
- this.logger.info(`\n⏭️ Skipping optional phase: ${phase.name} (not explicitly requested)`);
338
+ if (
339
+ phase.optional === true &&
340
+ this.phases &&
341
+ !this.phases.includes(phase.name)
342
+ ) {
343
+ this.logger.info(
344
+ `\n⏭️ Skipping optional phase: ${phase.name} (not explicitly requested)`,
345
+ );
306
346
  // Mark all commands in this phase as skipped
307
347
  phase.parallel.forEach(({ command }) => {
308
348
  this.skippedCommands.push(command);
349
+ this.skipReasons.set(command, 'optional_phase_not_requested');
309
350
  this.commandTimings.set(command, 0);
310
351
  });
311
352
  continue;
@@ -315,13 +356,14 @@ export class Orchestrator {
315
356
  // Mark all commands in remaining phases as skipped
316
357
  phase.parallel.forEach(({ command }) => {
317
358
  this.skippedCommands.push(command);
359
+ this.skipReasons.set(command, 'after_phase_failure');
318
360
  this.commandTimings.set(command, 0);
319
361
  });
320
362
  continue;
321
363
  }
322
364
 
323
- this.logger.info(`\n🔄 Starting phase: ${phase.name}`);
324
-
365
+ const phaseStartTime = Date.now();
366
+
325
367
  let results;
326
368
  if (this.sequential) {
327
369
  // Run commands sequentially
@@ -341,58 +383,90 @@ export class Orchestrator {
341
383
  );
342
384
  results = await Promise.all(tasks);
343
385
  }
344
-
345
- const phaseHasFailures = results.some(result => !result);
346
-
386
+
387
+ const phaseHasFailures = results.some((result) => !result);
388
+ const phaseDurationStr = `(${this.formatDuration(
389
+ Date.now() - phaseStartTime,
390
+ )})`;
391
+
347
392
  if (phaseHasFailures) {
348
393
  hasFailures = true;
349
394
  phaseFailed = true;
350
- this.logger.error(`❌ Phase "${phase.name}" completed with failures`);
395
+ this.logger.stopPhase(phase.name, false, phaseDurationStr);
351
396
  } else {
352
- this.logger.success(`✅ Phase "${phase.name}" completed successfully`);
397
+ this.logger.stopPhase(phase.name, true, phaseDurationStr);
353
398
  }
354
399
  }
355
400
  }
356
401
 
357
402
  // Validate start phase if specified
358
403
  if (this.startPhase && !startPhaseFound) {
359
- const availablePhases = this.config.phases.map(p => p.name).join(', ');
360
- this.logger.error(`❌ Start phase "${this.startPhase}" not found. Available phases: ${availablePhases}`);
404
+ const availablePhases = this.config.phases
405
+ .map((p) => p.name)
406
+ .join(', ');
407
+ this.logger.error(
408
+ `❌ Start phase "${this.startPhase}" not found. Available phases: ${availablePhases}`,
409
+ );
361
410
  process.exit(1);
362
411
  }
363
412
 
364
413
  // Validate phases if specified
365
414
  if (this.phases) {
366
- const availablePhases = this.config.phases.map(p => p.name);
367
- const invalidPhases = this.phases.filter(phase => !availablePhases.includes(phase));
415
+ const availablePhases = this.config.phases.map((p) => p.name);
416
+ const invalidPhases = this.phases.filter(
417
+ (phase) => !availablePhases.includes(phase),
418
+ );
368
419
  if (invalidPhases.length > 0) {
369
- this.logger.error(`❌ Invalid phases specified: ${invalidPhases.join(', ')}. Available phases: ${availablePhases.join(', ')}`);
420
+ this.logger.error(
421
+ `❌ Invalid phases specified: ${invalidPhases.join(', ')}. Available phases: ${availablePhases.join(', ')}`,
422
+ );
370
423
  process.exit(1);
371
424
  }
372
425
  }
373
426
 
374
427
  // Check final status
375
- hasFailures = hasFailures ||
376
- this.failedCommands.length > 0 ||
377
- this.skippedCommands.length > 0;
428
+ // Only count skipped commands as failures if they're due to dependency issues or phase failures
429
+ const failureSkippedCommands = Array.from(this.skipReasons.entries())
430
+ .filter(
431
+ ([, reason]) =>
432
+ reason === 'failed_dependency' || reason === 'after_phase_failure',
433
+ )
434
+ .map(([command]) => command);
435
+
436
+ hasFailures =
437
+ hasFailures ||
438
+ this.failedCommands.length > 0 ||
439
+ failureSkippedCommands.length > 0;
378
440
 
379
441
  // Add a small delay to ensure all processes have finished
380
442
  await new Promise((resolve) => setTimeout(resolve, 1000));
381
443
 
382
444
  this.summarizeResults();
383
-
445
+
384
446
  // Cleanup before exit since finally blocks don't run after process.exit()
385
447
  try {
386
448
  await this.processManager.cleanup();
387
449
  } catch (error) {
388
450
  this.logger.error(`Cleanup failed: ${error.message}`);
389
451
  }
390
-
452
+
453
+ // Log overall time after cleanup has finished
454
+ if (this.startTime) {
455
+ const overallDuration = Date.now() - this.startTime;
456
+ this.logger.printMessage(() =>
457
+ console.log(
458
+ chalk.cyan(
459
+ `[INFO] ⏱️ Overall time taken: ${this.formatDuration(overallDuration)}`,
460
+ ),
461
+ ),
462
+ );
463
+ }
464
+
391
465
  // Update git cache on successful execution
392
466
  if (!hasFailures) {
393
467
  await this.gitCache.updateCache();
394
468
  }
395
-
469
+
396
470
  // Force exit with appropriate status
397
471
  if (hasFailures) {
398
472
  this.logger.info('Exiting with failure status...');
@@ -403,15 +477,15 @@ export class Orchestrator {
403
477
  }
404
478
  } catch (error) {
405
479
  this.logger.error(`Orchestrator failed: ${error.message}`);
406
-
480
+
407
481
  // Cleanup on error
408
482
  try {
409
483
  await this.processManager.cleanup();
410
484
  } catch (cleanupError) {
411
485
  this.logger.error(`Cleanup failed: ${cleanupError.message}`);
412
486
  }
413
-
487
+
414
488
  process.exit(1);
415
489
  }
416
490
  }
417
- }
491
+ }
@@ -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,18 @@ 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
+ async runCommand({
59
+ cmd,
60
+ logFile,
61
+ background = false,
62
+ healthCheck = null,
63
+ kill_command = null,
64
+ isRetry = false,
65
+ env = null,
66
+ }) {
67
+ const baseDir = this.logFolder
68
+ ? path.resolve(this.logFolder)
69
+ : process.cwd();
42
70
  const LOGS_DIR = path.join(baseDir, 'scripts-orchestrator-logs');
43
71
  // Use only the first word of the command for the log filename
44
72
  const logName = cmd.split(/\s+/)[0];
@@ -54,7 +82,9 @@ export class ProcessManager {
54
82
  this.logger.verbose(`Clearing log file at ${LOG_FILE}`);
55
83
  fs.writeFileSync(LOG_FILE, ''); // Clear the log file
56
84
  } else {
57
- this.logger.verbose(`Appending to existing log file at ${LOG_FILE} (retry attempt)`);
85
+ this.logger.verbose(
86
+ `Appending to existing log file at ${LOG_FILE} (retry attempt)`,
87
+ );
58
88
  }
59
89
  } catch (error) {
60
90
  this.logger.error(`Failed to setup log file: ${error.message}`);
@@ -62,18 +92,21 @@ export class ProcessManager {
62
92
  }
63
93
 
64
94
  return new Promise((resolve) => {
95
+ const startTime = Date.now();
65
96
  // Build command with environment variables if provided
66
97
  let fullCommand = `npm run ${cmd}`;
67
98
  if (env && Object.keys(env).length > 0) {
68
- const envStr = Object.entries(env).map(([key, value]) => `${key}=${value}`).join(' ');
99
+ const envStr = Object.entries(env)
100
+ .map(([key, value]) => `${key}=${value}`)
101
+ .join(' ');
69
102
  fullCommand = `${envStr} npm run ${cmd}`;
70
103
  }
71
-
72
- this.logger.info(`Running: ${fullCommand}`);
73
-
104
+
105
+ this.logger.startTask(cmd, fullCommand);
106
+
74
107
  // Create isolated environment for each process
75
108
  const isolatedEnv = this.createIsolatedEnvironment({ command: cmd, env });
76
-
109
+
77
110
  const options = {
78
111
  shell: true,
79
112
  detached: background,
@@ -91,6 +124,7 @@ export class ProcessManager {
91
124
  const processInstance = spawn(fullCommand, [], options);
92
125
 
93
126
  processInstance.on('error', (error) => {
127
+ this.logger.stopTask(cmd);
94
128
  this.logger.error(`Failed to start process: ${error.message}`);
95
129
  //this.logger.verbose(`Process error details: ${JSON.stringify(error, null, 2)}`);
96
130
  resolve({ success: false, output: '' });
@@ -98,23 +132,29 @@ export class ProcessManager {
98
132
 
99
133
  if (background) {
100
134
  const processGroupId = processInstance.pid;
101
- this.logger.verbose(`Background process spawned with PID: ${processGroupId}`);
135
+ this.logger.verbose(
136
+ `Background process spawned with PID: ${processGroupId}`,
137
+ );
102
138
 
103
139
  // Track process exit for background processes
104
140
  let processExited = false;
105
141
  let processExitCode = null;
106
-
142
+
107
143
  processInstance.on('exit', (code, signal) => {
108
144
  processExited = true;
109
145
  processExitCode = code;
110
- this.logger.verbose(`Background process ${cmd} (PID: ${processGroupId}) exited with code: ${code}, signal: ${signal}`);
146
+ this.logger.verbose(
147
+ `Background process ${cmd} (PID: ${processGroupId}) exited with code: ${code}, signal: ${signal}`,
148
+ );
111
149
  });
112
150
 
113
151
  processInstance.stdout.on('data', (data) => {
114
152
  try {
115
153
  fs.appendFileSync(LOG_FILE, data.toString());
116
154
  } catch (error) {
117
- this.logger.error(`Failed to write to log file: ${error.message}`);
155
+ this.logger.error(
156
+ `Failed to write to log file: ${error.message}`,
157
+ );
118
158
  }
119
159
  });
120
160
 
@@ -122,7 +162,9 @@ export class ProcessManager {
122
162
  try {
123
163
  fs.appendFileSync(LOG_FILE, data.toString());
124
164
  } catch (error) {
125
- this.logger.error(`Failed to write to log file: ${error.message}`);
165
+ this.logger.error(
166
+ `Failed to write to log file: ${error.message}`,
167
+ );
126
168
  }
127
169
  });
128
170
 
@@ -134,37 +176,49 @@ export class ProcessManager {
134
176
  try {
135
177
  // First check if the process has already exited with an error
136
178
  if (processExited && processExitCode !== 0) {
137
- this.logger.error(`Background process ${cmd} exited with code ${processExitCode}`);
179
+ this.logger.stopTask(cmd);
180
+ this.logger.error(
181
+ `Background process ${cmd} exited with code ${processExitCode}`,
182
+ );
138
183
  let output = '';
139
184
  try {
140
185
  output = fs.readFileSync(LOG_FILE, 'utf8');
141
186
  this.logger.verbose(`Process output: ${output}`);
142
187
  } catch (error) {
143
- this.logger.error(`Failed to read log file: ${error.message}`);
188
+ this.logger.error(
189
+ `Failed to read log file: ${error.message}`,
190
+ );
144
191
  }
145
192
  return { success: false, output };
146
193
  }
147
-
148
- this.logger.verbose(`Verifying process ${processGroupId} (attempt ${attempt}/${maxAttempts})`);
194
+
195
+ this.logger.verbose(
196
+ `Verifying process ${processGroupId} (attempt ${attempt}/${maxAttempts})`,
197
+ );
149
198
  process.kill(processGroupId, 0);
150
199
  this.logger.verbose(`Process ${processGroupId} is running`);
151
-
200
+
152
201
  // Wait a bit more to ensure the process doesn't exit immediately after verification
153
202
  await new Promise((resolve) => setTimeout(resolve, 500));
154
-
203
+
155
204
  // Check again if the process exited during our wait
156
205
  if (processExited && processExitCode !== 0) {
157
- this.logger.error(`Background process ${cmd} exited with code ${processExitCode} shortly after starting`);
206
+ this.logger.stopTask(cmd);
207
+ this.logger.error(
208
+ `Background process ${cmd} exited with code ${processExitCode} shortly after starting`,
209
+ );
158
210
  let output = '';
159
211
  try {
160
212
  output = fs.readFileSync(LOG_FILE, 'utf8');
161
213
  this.logger.verbose(`Process output: ${output}`);
162
214
  } catch (error) {
163
- this.logger.error(`Failed to read log file: ${error.message}`);
215
+ this.logger.error(
216
+ `Failed to read log file: ${error.message}`,
217
+ );
164
218
  }
165
219
  return { success: false, output };
166
220
  }
167
-
221
+
168
222
  this.backgroundProcesses.push(processGroupId);
169
223
  this.backgroundProcessesDetails.push({
170
224
  command: cmd,
@@ -174,22 +228,31 @@ export class ProcessManager {
174
228
  startedByScript: true,
175
229
  kill_command,
176
230
  });
177
-
231
+
178
232
  this.logger.verbose(`Unreferencing process ${processGroupId}`);
179
233
  processInstance.unref();
180
-
234
+
235
+ this.logger.stopTask(cmd);
181
236
  this.logger.verbose(
182
237
  `Background process started: npm run ${cmd} (PGID: ${processGroupId})`,
183
238
  );
184
239
  return { success: true, output: '' };
185
240
  } catch (error) {
186
241
  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}`);
242
+ this.logger.error(
243
+ `Failed to start background process: npm run ${cmd}`,
244
+ );
245
+ this.logger.verbose(
246
+ `Final verification attempt failed: ${error.message}`,
247
+ );
189
248
  return { success: false, output: '' };
190
249
  }
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`);
250
+ this.logger.verbose(
251
+ `Verification attempt ${attempt} failed: ${error.message}`,
252
+ );
253
+ this.logger.verbose(
254
+ `Waiting ${baseDelay * Math.pow(2, attempt - 1)}ms before next attempt`,
255
+ );
193
256
  await new Promise((resolve) =>
194
257
  setTimeout(resolve, baseDelay * Math.pow(2, attempt - 1)),
195
258
  );
@@ -204,7 +267,9 @@ export class ProcessManager {
204
267
  try {
205
268
  fs.appendFileSync(LOG_FILE, data.toString());
206
269
  } catch (error) {
207
- this.logger.error(`Failed to write to log file: ${error.message}`);
270
+ this.logger.error(
271
+ `Failed to write to log file: ${error.message}`,
272
+ );
208
273
  }
209
274
  });
210
275
 
@@ -212,7 +277,9 @@ export class ProcessManager {
212
277
  try {
213
278
  fs.appendFileSync(LOG_FILE, data.toString());
214
279
  } catch (error) {
215
- this.logger.error(`Failed to write to log file: ${error.message}`);
280
+ this.logger.error(
281
+ `Failed to write to log file: ${error.message}`,
282
+ );
216
283
  }
217
284
  });
218
285
 
@@ -224,17 +291,25 @@ export class ProcessManager {
224
291
  this.logger.error(`Failed to read log file: ${error.message}`);
225
292
  }
226
293
 
294
+ this.logger.stopTask(cmd);
295
+
296
+ const duration = Date.now() - startTime;
297
+ const durationStr = ` (${this.formatDuration(duration)})`;
298
+
227
299
  if (code !== 0) {
228
- this.logger.error(`Failed: npm run ${cmd} (exit code: ${code})`);
300
+ this.logger.error(
301
+ `Failed: npm run ${cmd} ❌${durationStr} (exit code: ${code})`,
302
+ );
229
303
  this.logger.verbose(`Process output: ${output}`);
230
304
  resolve({ success: false, output });
231
305
  } else {
232
- this.logger.success(`Completed: npm run ${cmd}`);
306
+ this.logger.success(`Completed: npm run ${cmd} ✅${durationStr}`);
233
307
  resolve({ success: true, output });
234
308
  }
235
309
  });
236
310
  }
237
311
  } catch (error) {
312
+ this.logger.stopTask(cmd);
238
313
  this.logger.error(`Failed to spawn process: ${error.message}`);
239
314
  //this.logger.verbose(`Spawn error details: ${JSON.stringify(error, null, 2)}`);
240
315
  resolve({ success: false, output: '' });
@@ -245,7 +320,7 @@ export class ProcessManager {
245
320
  createIsolatedEnvironment({ command, env = null }) {
246
321
  // Create a deep copy to avoid any reference sharing
247
322
  const baseEnv = JSON.parse(JSON.stringify(process.env));
248
-
323
+
249
324
  // Set standard environment variables
250
325
  const isolatedEnv = {
251
326
  ...baseEnv,
@@ -272,25 +347,37 @@ export class ProcessManager {
272
347
  // Remove any potentially problematic environment variables
273
348
  delete isolatedEnv.npm_lifecycle_event;
274
349
  delete isolatedEnv.npm_lifecycle_script;
275
-
350
+
276
351
  return isolatedEnv;
277
352
  }
278
353
 
279
354
  async cleanup() {
280
355
  try {
281
356
  this.logger.info('\nCleaning up background processes...');
282
-
357
+
283
358
  // Debug: Log the number of processes we're tracking
284
- this.logger.info(`- Found ${this.backgroundProcessesDetails.length} background processes to clean up`);
285
-
359
+ this.logger.info(
360
+ `- Found ${this.backgroundProcessesDetails.length} background processes to clean up`,
361
+ );
362
+
286
363
  // 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
-
364
+ this.backgroundProcessesDetails.forEach(
365
+ ({ command, pgid, url, startedByScript, kill_command }, index) => {
366
+ this.logger.verbose(
367
+ `- Process ${index + 1}: command=${command}, pgid=${pgid}, url=${url}, startedByScript=${startedByScript}, kill_command=${kill_command}`,
368
+ );
369
+ },
370
+ );
371
+
291
372
  const killPromises = this.backgroundProcessesDetails.map(
292
373
  async ({ command, pgid, url, startedByScript, kill_command }) => {
293
- await this.cleanupProcess({ command, pgid, url, startedByScript, kill_command });
374
+ await this.cleanupProcess({
375
+ command,
376
+ pgid,
377
+ url,
378
+ startedByScript,
379
+ kill_command,
380
+ });
294
381
  },
295
382
  );
296
383
 
@@ -304,33 +391,43 @@ export class ProcessManager {
304
391
 
305
392
  async cleanupCommand(commandName) {
306
393
  this.logger.info(`\nCleaning up processes for command: ${commandName}`);
307
-
394
+
308
395
  // Find processes for this specific command
309
396
  const commandProcesses = this.backgroundProcessesDetails.filter(
310
- ({ command }) => command === commandName
397
+ ({ command }) => command === commandName,
311
398
  );
312
-
399
+
313
400
  if (commandProcesses.length === 0) {
314
- this.logger.verbose(`- No background processes found for command: ${commandName}`);
401
+ this.logger.verbose(
402
+ `- No background processes found for command: ${commandName}`,
403
+ );
315
404
  return;
316
405
  }
317
-
318
- this.logger.verbose(`- Found ${commandProcesses.length} background processes for command: ${commandName}`);
319
-
406
+
407
+ this.logger.verbose(
408
+ `- Found ${commandProcesses.length} background processes for command: ${commandName}`,
409
+ );
410
+
320
411
  const killPromises = commandProcesses.map(
321
412
  async ({ command, pgid, url, startedByScript, kill_command }) => {
322
- await this.cleanupProcess({ command, pgid, url, startedByScript, kill_command });
323
- }
413
+ await this.cleanupProcess({
414
+ command,
415
+ pgid,
416
+ url,
417
+ startedByScript,
418
+ kill_command,
419
+ });
420
+ },
324
421
  );
325
422
 
326
423
  await Promise.allSettled(killPromises);
327
-
424
+
328
425
  // Remove the cleaned up processes from our tracking arrays
329
- this.backgroundProcesses = this.backgroundProcesses.filter(pgid =>
330
- !commandProcesses.some(proc => proc.pgid === pgid)
426
+ this.backgroundProcesses = this.backgroundProcesses.filter(
427
+ (pgid) => !commandProcesses.some((proc) => proc.pgid === pgid),
331
428
  );
332
429
  this.backgroundProcessesDetails = this.backgroundProcessesDetails.filter(
333
- ({ command }) => command !== commandName
430
+ ({ command }) => command !== commandName,
334
431
  );
335
432
  }
336
433
 
@@ -342,24 +439,40 @@ export class ProcessManager {
342
439
  return;
343
440
  }
344
441
 
345
- this.logger.verbose(`- Processing cleanup for ${command} (kill_command: ${kill_command})`);
442
+ this.logger.verbose(
443
+ `- Processing cleanup for ${command} (kill_command: ${kill_command})`,
444
+ );
346
445
 
347
446
  // Try custom kill command first if specified
348
447
  if (kill_command) {
349
448
  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 });
449
+ this.logger.verbose(
450
+ `- Using custom kill command: npm run ${kill_command}`,
451
+ );
452
+ const result = await this.runCommand({
453
+ cmd: kill_command,
454
+ logFile: null,
455
+ background: false,
456
+ });
352
457
  if (result.success) {
353
- this.logger.verbose(`- Successfully killed ${command} using custom command`);
458
+ this.logger.verbose(
459
+ `- Successfully killed ${command} using custom command`,
460
+ );
354
461
  return;
355
462
  } else {
356
- this.logger.verbose('- Custom kill command failed, falling back to process signals');
463
+ this.logger.verbose(
464
+ '- Custom kill command failed, falling back to process signals',
465
+ );
357
466
  }
358
467
  } catch (error) {
359
- this.logger.verbose(`- Custom kill command error: ${error.message}, falling back`);
468
+ this.logger.verbose(
469
+ `- Custom kill command error: ${error.message}, falling back`,
470
+ );
360
471
  }
361
472
  } else {
362
- this.logger.verbose(`- No kill_command specified for ${command}, using process signals`);
473
+ this.logger.verbose(
474
+ `- No kill_command specified for ${command}, using process signals`,
475
+ );
363
476
  }
364
477
 
365
478
  try {
@@ -375,27 +488,36 @@ export class ProcessManager {
375
488
 
376
489
  // Cross-platform process termination
377
490
  const isWindows = process.platform === 'win32';
378
-
491
+
379
492
  if (isWindows) {
380
493
  // Windows: use taskkill to terminate process tree
381
494
  try {
382
- const killProcess = spawn('taskkill', ['/F', '/T', '/PID', pgid.toString()]);
495
+ const killProcess = spawn('taskkill', [
496
+ '/F',
497
+ '/T',
498
+ '/PID',
499
+ pgid.toString(),
500
+ ]);
383
501
  await new Promise((resolve) => {
384
502
  killProcess.on('close', resolve);
385
503
  });
386
- this.logger.verbose(`- Terminated background process: ${command} (PID: ${pgid})`);
504
+ this.logger.verbose(
505
+ `- Terminated background process: ${command} (PID: ${pgid})`,
506
+ );
387
507
  return;
388
508
  } catch (killError) {
389
- this.logger.verbose(`- Failed to use taskkill, falling back to process.kill: ${killError.message}`);
509
+ this.logger.verbose(
510
+ `- Failed to use taskkill, falling back to process.kill: ${killError.message}`,
511
+ );
390
512
  }
391
513
  }
392
-
514
+
393
515
  // Unix/Linux/macOS or Windows fallback: Try SIGTERM first
394
516
  process.kill(pgid, 'SIGTERM');
395
517
 
396
518
  await new Promise((resolve, reject) => {
397
519
  let timeout, checkInterval;
398
-
520
+
399
521
  timeout = setTimeout(() => {
400
522
  if (checkInterval) clearInterval(checkInterval);
401
523
  reject(new Error('Process termination timeout'));
@@ -415,26 +537,31 @@ export class ProcessManager {
415
537
  `- Terminated background process: ${command} (PGID: ${pgid})`,
416
538
  );
417
539
  } catch (error) {
418
- this.logger.verbose(`- Failed to terminate process group: ${error.message}`);
540
+ this.logger.verbose(
541
+ `- Failed to terminate process group: ${error.message}`,
542
+ );
419
543
  }
420
544
 
421
545
  // Check if the URL is still responding after termination attempt
422
546
  if (url) {
423
547
  try {
424
548
  const urlObj = new URL(url);
425
- const port = urlObj.port || (urlObj.protocol === 'https:' ? '443' : '80');
426
-
549
+ const port =
550
+ urlObj.port || (urlObj.protocol === 'https:' ? '443' : '80');
551
+
427
552
  // Use shared HTTP utility for cross-platform compatibility
428
553
  const urlResult = await HealthCheck.makeHttpRequest(url, 2000);
429
-
554
+
430
555
  if (urlResult.success && urlResult.statusCode === 200) {
431
- this.logger.verbose(`- URL ${url} is still responding after termination, finding process on port ${port}`);
432
-
556
+ this.logger.verbose(
557
+ `- URL ${url} is still responding after termination, finding process on port ${port}`,
558
+ );
559
+
433
560
  // Find and kill process using the port - cross-platform approach
434
561
  try {
435
562
  const isWindows = process.platform === 'win32';
436
563
  let findPortCmd, findPortArgs;
437
-
564
+
438
565
  if (isWindows) {
439
566
  // Windows: use netstat
440
567
  findPortCmd = 'netstat';
@@ -444,7 +571,7 @@ export class ProcessManager {
444
571
  findPortCmd = 'lsof';
445
572
  findPortArgs = ['-i', `:${port}`, '-t'];
446
573
  }
447
-
574
+
448
575
  const findProcess = spawn(findPortCmd, findPortArgs);
449
576
  const result = await new Promise((resolve) => {
450
577
  let output = '';
@@ -458,12 +585,15 @@ export class ProcessManager {
458
585
 
459
586
  if (result.code === 0 && result.output.trim()) {
460
587
  let pids = [];
461
-
588
+
462
589
  if (isWindows) {
463
590
  // Parse netstat output to find PIDs for the specific port
464
591
  const lines = result.output.split('\n');
465
592
  for (const line of lines) {
466
- if (line.includes(`:${port} `) && line.includes('LISTENING')) {
593
+ if (
594
+ line.includes(`:${port} `) &&
595
+ line.includes('LISTENING')
596
+ ) {
467
597
  const parts = line.trim().split(/\s+/);
468
598
  const pid = parts[parts.length - 1];
469
599
  if (pid && !isNaN(pid)) {
@@ -475,7 +605,7 @@ export class ProcessManager {
475
605
  // lsof output is already just PIDs
476
606
  pids = result.output.trim().split('\n');
477
607
  }
478
-
608
+
479
609
  for (const pid of pids) {
480
610
  try {
481
611
  if (isWindows) {
@@ -488,16 +618,22 @@ export class ProcessManager {
488
618
  // Unix/Linux/macOS: use process.kill
489
619
  process.kill(parseInt(pid), 'SIGKILL');
490
620
  }
491
- this.logger.verbose(`- Killed process (PID: ${pid}) using port ${port}`);
621
+ this.logger.verbose(
622
+ `- Killed process (PID: ${pid}) using port ${port}`,
623
+ );
492
624
  } catch (killError) {
493
625
  if (killError.code !== 'ESRCH') {
494
- this.logger.error(`- Failed to kill process (PID: ${pid}): ${killError.message}`);
626
+ this.logger.error(
627
+ `- Failed to kill process (PID: ${pid}): ${killError.message}`,
628
+ );
495
629
  }
496
630
  }
497
631
  }
498
632
  }
499
633
  } catch (portError) {
500
- this.logger.error(`- Failed to find process using port ${port}: ${portError.message}`);
634
+ this.logger.error(
635
+ `- Failed to find process using port ${port}: ${portError.message}`,
636
+ );
501
637
  }
502
638
  }
503
639
  } catch (error) {
@@ -508,10 +644,15 @@ export class ProcessManager {
508
644
  // Final attempt to kill the process group
509
645
  try {
510
646
  const isWindows = process.platform === 'win32';
511
-
647
+
512
648
  if (isWindows) {
513
649
  // Windows: force kill with taskkill
514
- const killProcess = spawn('taskkill', ['/F', '/T', '/PID', pgid.toString()]);
650
+ const killProcess = spawn('taskkill', [
651
+ '/F',
652
+ '/T',
653
+ '/PID',
654
+ pgid.toString(),
655
+ ]);
515
656
  await new Promise((resolve) => {
516
657
  killProcess.on('close', resolve);
517
658
  });
@@ -528,4 +669,4 @@ export class ProcessManager {
528
669
  }
529
670
 
530
671
  // For backward compatibility
531
- export const processManager = new ProcessManager();
672
+ export const processManager = new ProcessManager();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scripts-orchestrator",
3
- "version": "2.10.0",
3
+ "version": "2.12.0",
4
4
  "description": "A powerful script orchestrator for running parallel commands with dependency management, background processes, and health checks",
5
5
  "main": "lib/index.js",
6
6
  "type": "module",