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.
package/README.md CHANGED
@@ -263,6 +263,9 @@ The orchestrator doesn't care what the commands do - it just ensures they run (i
263
263
 
264
264
  # Specify a custom log folder
265
265
  npm run scripts-orchestrator -- --logFolder ./custom-logs
266
+
267
+ # Force execution even if git state is unchanged
268
+ npm run scripts-orchestrator -- --force
266
269
  ```
267
270
 
268
271
  ### Starting from a Specific Phase
@@ -422,6 +425,21 @@ This feature is particularly useful in CI/CD pipelines where the same commit mig
422
425
 
423
426
  **Note**: The cache is only updated on successful execution. Failed runs will not update the cache, ensuring subsequent runs will retry.
424
427
 
428
+ ### Force Execution
429
+
430
+ You can bypass the git cache check and force execution even when the git state is unchanged by using the `--force` flag:
431
+
432
+ ```bash
433
+ # Force execution regardless of git state
434
+ npm run scripts-orchestrator -- --force
435
+ ```
436
+
437
+ This is useful when you want to:
438
+ - Re-run commands without making code changes
439
+ - Test configuration changes
440
+ - Debug issues without modifying the codebase
441
+ - Override the cache in CI/CD pipelines
442
+
425
443
  ## Exit Codes
426
444
 
427
445
  - `0`: All commands executed successfully
package/index.js CHANGED
@@ -35,6 +35,10 @@ const argv = yargs(hideBin(process.argv))
35
35
  type: 'boolean',
36
36
  description: 'Run all commands sequentially instead of in parallel (for low CPU machines)',
37
37
  })
38
+ .option('force', {
39
+ type: 'boolean',
40
+ description: 'Force execution even if git state is unchanged',
41
+ })
38
42
  .help()
39
43
  .alias('h', 'help')
40
44
  .parse();
@@ -46,6 +50,7 @@ let startPhase = argv.phase;
46
50
  let logFolder = argv.logFolder;
47
51
  const phases = argv.phases ? argv.phases.split(',').map(p => p.trim()) : null;
48
52
  const sequential = argv.sequential || false;
53
+ const force = argv.force || false;
49
54
 
50
55
  // Validate config file exists
51
56
  if (!fs.existsSync(configPath)) {
@@ -75,7 +80,7 @@ if (logFolder) {
75
80
  }
76
81
 
77
82
  // Create and run the orchestrator
78
- const orchestrator = new Orchestrator(commandsConfig, startPhase, logFolder, phases, sequential);
83
+ const orchestrator = new Orchestrator(commandsConfig, startPhase, logFolder, phases, sequential, force);
79
84
 
80
85
  // Enhanced signal handlers
81
86
  const handleSignal = async (signal) => {
@@ -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 };