scripts-orchestrator 2.9.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.
@@ -2,27 +2,37 @@ 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) {
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;
11
19
  this.phases = phases;
12
20
  this.sequential = sequential;
21
+ this.force = force;
13
22
  this.processManager = processManager;
14
23
  this.healthCheck = healthCheck;
15
24
  this.logger = log;
16
25
  this.failedCommands = [];
17
26
  this.skippedCommands = [];
27
+ this.skipReasons = new Map(); // Track why commands were skipped
18
28
  this.commandTimings = new Map();
19
29
  this.gitCache = new GitCache(logFolder);
20
-
30
+
21
31
  // Set the log folder in process manager
22
32
  if (logFolder) {
23
33
  this.processManager.setLogFolder(logFolder);
24
34
  }
25
-
35
+
26
36
  // Flatten commands for easier tracking
27
37
  this.allCommands = this.flattenCommands(config);
28
38
  }
@@ -32,11 +42,11 @@ export class Orchestrator {
32
42
  if (Array.isArray(config)) {
33
43
  return config;
34
44
  }
35
-
45
+
36
46
  if (config.phases) {
37
- return config.phases.flatMap(phase => phase.parallel || []);
47
+ return config.phases.flatMap((phase) => phase.parallel || []);
38
48
  }
39
-
49
+
40
50
  return [];
41
51
  }
42
52
 
@@ -85,6 +95,7 @@ export class Orchestrator {
85
95
  if (status === 'disabled') {
86
96
  this.logger.warn(`Skipping: npm run ${command} (status: disabled)`);
87
97
  this.skippedCommands.push(command);
98
+ this.skipReasons.set(command, 'disabled');
88
99
  this.commandTimings.set(command, Date.now() - startTime);
89
100
  visited.delete(command);
90
101
  return true;
@@ -92,10 +103,23 @@ export class Orchestrator {
92
103
 
93
104
  const checkUrl = health_check?.url;
94
105
  if (checkUrl) {
95
- this.logger.info(`Checking if ${checkUrl} is already available...`);
96
- const urlAvailable = await this.healthCheck.waitForUrl({url: checkUrl, maxAttempts: 1, silent:true});
97
- if (urlAvailable) {
98
- 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
+ );
99
123
  this.processManager.addBackgroundProcess({
100
124
  command,
101
125
  url: checkUrl,
@@ -115,23 +139,21 @@ export class Orchestrator {
115
139
  if (!dependencySuccess) {
116
140
  this.logger.error(`Skipping ${command} due to failed dependency`);
117
141
  this.skippedCommands.push(command);
142
+ this.skipReasons.set(command, 'failed_dependency');
118
143
  this.commandTimings.set(command, Date.now() - startTime);
119
144
  visited.delete(command);
120
145
  return false;
121
146
  }
122
147
 
123
148
  if (dependency.health_check?.url) {
124
- this.logger.info(`Waiting for ${dependency.health_check.url} to be available...`);
125
149
  const urlAvailable = await this.healthCheck.waitForUrl({
126
150
  url: dependency.health_check.url,
127
151
  maxAttempts: dependency.health_check?.max_attempts || 20,
128
152
  interval: dependency.health_check?.interval || 2000,
129
153
  });
130
154
  if (!urlAvailable) {
131
- this.logger.error(
132
- `URL ${dependency.health_check.url} is not available after maximum attempts`,
133
- );
134
155
  this.skippedCommands.push(command);
156
+ this.skipReasons.set(command, 'failed_dependency');
135
157
  this.commandTimings.set(command, Date.now() - startTime);
136
158
  visited.delete(command);
137
159
  return false;
@@ -140,7 +162,9 @@ export class Orchestrator {
140
162
  this.logger.verbose(`Waiting ${dependency.wait}ms`);
141
163
  await new Promise((resolve) => {
142
164
  setTimeout(() => {
143
- 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
+ );
144
168
  resolve(true);
145
169
  }, dependency.wait);
146
170
  });
@@ -152,10 +176,12 @@ export class Orchestrator {
152
176
  let result = false;
153
177
  let commandOutput = '';
154
178
  let commandFailed = false;
155
-
179
+
156
180
  for (let attempt = 1; attempt <= attempts; attempt++) {
157
181
  if (attempt > 1) {
158
- this.logger.warn(`Retrying ${command} (attempt ${attempt}/${attempts})`);
182
+ this.logger.warn(
183
+ `Retrying ${command} (attempt ${attempt}/${attempts})`,
184
+ );
159
185
  await new Promise((resolve) => setTimeout(resolve, 1000));
160
186
  }
161
187
 
@@ -173,7 +199,9 @@ export class Orchestrator {
173
199
 
174
200
  if (result) {
175
201
  // Remove from failed commands if it was there
176
- this.failedCommands = this.failedCommands.filter(cmd => cmd !== command);
202
+ this.failedCommands = this.failedCommands.filter(
203
+ (cmd) => cmd !== command,
204
+ );
177
205
  commandFailed = false;
178
206
  break;
179
207
  } else if (attempt < attempts) {
@@ -184,7 +212,9 @@ export class Orchestrator {
184
212
  commandFailed = true;
185
213
  break;
186
214
  }
187
- this.logger.error(`Attempt ${attempt}/${attempts} failed for ${command}`);
215
+ this.logger.error(
216
+ `Attempt ${attempt}/${attempts} failed for ${command}`,
217
+ );
188
218
  commandFailed = true;
189
219
  } else {
190
220
  commandFailed = true;
@@ -193,14 +223,18 @@ export class Orchestrator {
193
223
 
194
224
  if (commandFailed) {
195
225
  this.failedCommands.push(command);
196
-
226
+
197
227
  // Cleanup any background processes for this failed command
198
228
  if (background) {
199
- 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
+ );
200
232
  try {
201
233
  await this.processManager.cleanupCommand(command);
202
234
  } catch (cleanupError) {
203
- 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
+ );
204
238
  }
205
239
  }
206
240
  }
@@ -211,40 +245,45 @@ export class Orchestrator {
211
245
  }
212
246
 
213
247
  summarizeResults() {
214
- this.logger.info('\nCommand Summary:');
215
248
  let hasFailures = false;
216
-
249
+
250
+ // Check if any command failed or was skipped due to failure
217
251
  this.allCommands.forEach(({ command }) => {
218
- const duration = this.commandTimings.get(command);
219
- const durationStr = duration ? ` (${this.formatDuration(duration)})` : '';
220
-
221
252
  if (this.failedCommands.includes(command)) {
222
253
  hasFailures = true;
223
- // Get the actual log path from process manager
224
- const logPath = this.processManager.getLogPath(command);
225
- this.logger.error(`- ${command}: ❌${durationStr} (See ${logPath})`);
226
254
  } else if (this.skippedCommands.includes(command)) {
227
- hasFailures = true;
228
- this.logger.warn(`- ${command}: ⚠️${durationStr} (Skipped due to failed dependency)`);
229
- } else {
230
- 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
+ }
231
262
  }
232
263
  });
233
264
 
234
265
  if (hasFailures) {
235
- this.logger.error('\n❌ Some commands failed or were skipped. See details above.');
266
+ this.logger.error('\n❌ Some commands failed or were skipped.');
236
267
  } else {
237
268
  this.logger.success('\n🎉 All commands executed successfully!');
238
269
  }
239
270
  }
240
271
 
241
272
  async run() {
273
+ this.startTime = Date.now();
242
274
  try {
243
- // Check if we should skip execution based on git state
244
- const shouldSkip = await this.gitCache.shouldSkipExecution();
245
- if (shouldSkip) {
246
- this.logger.success('🎉 No changes detected, skipping execution!');
247
- process.exit(0);
275
+ // Check if we should skip execution based on git state (unless forced)
276
+ if (!this.force) {
277
+ const shouldSkip = await this.gitCache.shouldSkipExecution();
278
+ if (shouldSkip) {
279
+ this.logger.success('🎉 No changes detected, skipping execution!');
280
+ this.logger.info('💡 To force execution, use: --force');
281
+ process.exit(0);
282
+ }
283
+ } else {
284
+ this.logger.info(
285
+ '⚡ Force execution enabled, skipping git cache check',
286
+ );
248
287
  }
249
288
 
250
289
  let hasFailures = false;
@@ -270,14 +309,14 @@ export class Orchestrator {
270
309
  this.executeCommand(commandConfig),
271
310
  );
272
311
  const results = await Promise.all(tasks);
273
- hasFailures = results.some(result => !result);
312
+ hasFailures = results.some((result) => !result);
274
313
  }
275
314
  } else if (this.config.phases) {
276
315
  // New: Run phases sequentially, commands within phases in parallel or sequential based on flag
277
316
  if (this.sequential) {
278
317
  this.logger.info('🔄 Running in sequential mode');
279
318
  }
280
-
319
+
281
320
  for (const phase of this.config.phases) {
282
321
  // Check if we should start from this phase
283
322
  if (this.startPhase && !startPhaseFound) {
@@ -288,6 +327,7 @@ export class Orchestrator {
288
327
  // Mark all commands in previous phases as skipped
289
328
  phase.parallel.forEach(({ command }) => {
290
329
  this.skippedCommands.push(command);
330
+ this.skipReasons.set(command, 'before_start_phase');
291
331
  this.commandTimings.set(command, 0);
292
332
  });
293
333
  continue;
@@ -295,11 +335,18 @@ export class Orchestrator {
295
335
  }
296
336
 
297
337
  // Check if this is an optional phase that should be skipped
298
- if (phase.optional === true && this.phases && !this.phases.includes(phase.name)) {
299
- 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
+ );
300
346
  // Mark all commands in this phase as skipped
301
347
  phase.parallel.forEach(({ command }) => {
302
348
  this.skippedCommands.push(command);
349
+ this.skipReasons.set(command, 'optional_phase_not_requested');
303
350
  this.commandTimings.set(command, 0);
304
351
  });
305
352
  continue;
@@ -309,13 +356,14 @@ export class Orchestrator {
309
356
  // Mark all commands in remaining phases as skipped
310
357
  phase.parallel.forEach(({ command }) => {
311
358
  this.skippedCommands.push(command);
359
+ this.skipReasons.set(command, 'after_phase_failure');
312
360
  this.commandTimings.set(command, 0);
313
361
  });
314
362
  continue;
315
363
  }
316
364
 
317
- this.logger.info(`\n🔄 Starting phase: ${phase.name}`);
318
-
365
+ const phaseStartTime = Date.now();
366
+
319
367
  let results;
320
368
  if (this.sequential) {
321
369
  // Run commands sequentially
@@ -335,58 +383,90 @@ export class Orchestrator {
335
383
  );
336
384
  results = await Promise.all(tasks);
337
385
  }
338
-
339
- const phaseHasFailures = results.some(result => !result);
340
-
386
+
387
+ const phaseHasFailures = results.some((result) => !result);
388
+ const phaseDurationStr = `(${this.formatDuration(
389
+ Date.now() - phaseStartTime,
390
+ )})`;
391
+
341
392
  if (phaseHasFailures) {
342
393
  hasFailures = true;
343
394
  phaseFailed = true;
344
- this.logger.error(`❌ Phase "${phase.name}" completed with failures`);
395
+ this.logger.stopPhase(phase.name, false, phaseDurationStr);
345
396
  } else {
346
- this.logger.success(`✅ Phase "${phase.name}" completed successfully`);
397
+ this.logger.stopPhase(phase.name, true, phaseDurationStr);
347
398
  }
348
399
  }
349
400
  }
350
401
 
351
402
  // Validate start phase if specified
352
403
  if (this.startPhase && !startPhaseFound) {
353
- const availablePhases = this.config.phases.map(p => p.name).join(', ');
354
- 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
+ );
355
410
  process.exit(1);
356
411
  }
357
412
 
358
413
  // Validate phases if specified
359
414
  if (this.phases) {
360
- const availablePhases = this.config.phases.map(p => p.name);
361
- 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
+ );
362
419
  if (invalidPhases.length > 0) {
363
- 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
+ );
364
423
  process.exit(1);
365
424
  }
366
425
  }
367
426
 
368
427
  // Check final status
369
- hasFailures = hasFailures ||
370
- this.failedCommands.length > 0 ||
371
- 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;
372
440
 
373
441
  // Add a small delay to ensure all processes have finished
374
442
  await new Promise((resolve) => setTimeout(resolve, 1000));
375
443
 
376
444
  this.summarizeResults();
377
-
445
+
378
446
  // Cleanup before exit since finally blocks don't run after process.exit()
379
447
  try {
380
448
  await this.processManager.cleanup();
381
449
  } catch (error) {
382
450
  this.logger.error(`Cleanup failed: ${error.message}`);
383
451
  }
384
-
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
+
385
465
  // Update git cache on successful execution
386
466
  if (!hasFailures) {
387
467
  await this.gitCache.updateCache();
388
468
  }
389
-
469
+
390
470
  // Force exit with appropriate status
391
471
  if (hasFailures) {
392
472
  this.logger.info('Exiting with failure status...');
@@ -397,15 +477,15 @@ export class Orchestrator {
397
477
  }
398
478
  } catch (error) {
399
479
  this.logger.error(`Orchestrator failed: ${error.message}`);
400
-
480
+
401
481
  // Cleanup on error
402
482
  try {
403
483
  await this.processManager.cleanup();
404
484
  } catch (cleanupError) {
405
485
  this.logger.error(`Cleanup failed: ${cleanupError.message}`);
406
486
  }
407
-
487
+
408
488
  process.exit(1);
409
489
  }
410
490
  }
411
- }
491
+ }