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.
package/index.js CHANGED
@@ -39,6 +39,18 @@ const argv = yargs(hideBin(process.argv))
39
39
  type: 'boolean',
40
40
  description: 'Force execution even if git state is unchanged',
41
41
  })
42
+ .option('metrics', {
43
+ type: 'string',
44
+ description: 'Comma-separated metrics to collect and report: time, memory',
45
+ })
46
+ .option('json-results', {
47
+ type: 'string',
48
+ description: 'Write results JSON to this path; use "-" for stdout only',
49
+ })
50
+ .option('html-results', {
51
+ type: 'string',
52
+ description: 'Write HTML report to this path; use "-" for stdout only',
53
+ })
42
54
  .help()
43
55
  .alias('h', 'help')
44
56
  .parse();
@@ -52,6 +64,8 @@ const phases = argv.phases ? argv.phases.split(',').map(p => p.trim()) : null;
52
64
  const sequential = argv.sequential || false;
53
65
  const force = argv.force || false;
54
66
 
67
+ const validMetrics = ['time', 'memory'];
68
+
55
69
  // Validate config file exists
56
70
  if (!fs.existsSync(configPath)) {
57
71
  log.error(`Error: Config file not found at ${configPath}`);
@@ -74,13 +88,46 @@ if (!logFolder && commandsConfig.log_folder) {
74
88
  logFolder = commandsConfig.log_folder;
75
89
  }
76
90
 
91
+ // Metrics: CLI overrides config
92
+ let metrics = [];
93
+ if (argv.metrics != null && argv.metrics !== '') {
94
+ metrics = argv.metrics.split(',').map((m) => m.trim()).filter((m) => validMetrics.includes(m));
95
+ } else if (commandsConfig.metrics != null) {
96
+ const fromConfig = Array.isArray(commandsConfig.metrics)
97
+ ? commandsConfig.metrics
98
+ : String(commandsConfig.metrics).split(',').map((m) => m.trim());
99
+ metrics = fromConfig.filter((m) => validMetrics.includes(m));
100
+ }
101
+
102
+ // JSON results path: CLI overrides config
103
+ const jsonResultsPath =
104
+ argv.jsonResults != null
105
+ ? argv.jsonResults
106
+ : (commandsConfig.json_results ?? commandsConfig.json_results_path ?? null);
107
+
108
+ // HTML results path: CLI overrides config (optional)
109
+ const htmlResultsPath =
110
+ argv.htmlResults != null
111
+ ? argv.htmlResults
112
+ : (commandsConfig.html_results ?? commandsConfig.html_results_path ?? null);
113
+
77
114
  // Set the log folder for the main orchestrator logs if specified
78
115
  if (logFolder) {
79
116
  log.setLogFolder(logFolder);
80
117
  }
81
118
 
82
119
  // Create and run the orchestrator
83
- const orchestrator = new Orchestrator(commandsConfig, startPhase, logFolder, phases, sequential, force);
120
+ const orchestrator = new Orchestrator(
121
+ commandsConfig,
122
+ startPhase,
123
+ logFolder,
124
+ phases,
125
+ sequential,
126
+ force,
127
+ metrics,
128
+ jsonResultsPath,
129
+ htmlResultsPath,
130
+ );
84
131
 
85
132
  // Enhanced signal handlers
86
133
  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 };