scripts-orchestrator 2.12.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,3 +1,4 @@
1
+ import fs from 'fs';
1
2
  import { processManager } from './process-manager.js';
2
3
  import { healthCheck } from './health-check.js';
3
4
  import { log } from './logger.js';
@@ -12,6 +13,9 @@ export class Orchestrator {
12
13
  phases = null,
13
14
  sequential = false,
14
15
  force = false,
16
+ metrics = [],
17
+ jsonResultsPath = null,
18
+ htmlResultsPath = null,
15
19
  ) {
16
20
  this.config = config;
17
21
  this.startPhase = startPhase;
@@ -19,13 +23,17 @@ export class Orchestrator {
19
23
  this.phases = phases;
20
24
  this.sequential = sequential;
21
25
  this.force = force;
26
+ this.metrics = Array.isArray(metrics) ? metrics : [];
27
+ this.jsonResultsPath = jsonResultsPath ?? null;
28
+ this.htmlResultsPath = htmlResultsPath ?? null;
22
29
  this.processManager = processManager;
23
30
  this.healthCheck = healthCheck;
24
31
  this.logger = log;
25
32
  this.failedCommands = [];
26
33
  this.skippedCommands = [];
27
34
  this.skipReasons = new Map(); // Track why commands were skipped
28
- this.commandTimings = new Map();
35
+ this.commandTimings = new Map(); // command -> { durationMs, memoryKb? }
36
+ this.phaseResults = []; // { name, success, durationMs } per phase run
29
37
  this.gitCache = new GitCache(logFolder);
30
38
 
31
39
  // Set the log folder in process manager
@@ -80,13 +88,17 @@ export class Orchestrator {
80
88
 
81
89
  const startTime = Date.now();
82
90
 
91
+ const setTiming = (durationMs, memoryKb = null) => {
92
+ this.commandTimings.set(command, { durationMs, memoryKb });
93
+ };
94
+
83
95
  // Check for circular dependencies
84
96
  if (visited.has(command)) {
85
97
  this.logger.error(
86
98
  `Circular dependency detected: ${Array.from(visited).join(' -> ')} -> ${command}`,
87
99
  );
88
100
  this.failedCommands.push(command);
89
- this.commandTimings.set(command, Date.now() - startTime);
101
+ setTiming(Date.now() - startTime);
90
102
  return false;
91
103
  }
92
104
  visited.add(command);
@@ -96,7 +108,7 @@ export class Orchestrator {
96
108
  this.logger.warn(`Skipping: npm run ${command} (status: disabled)`);
97
109
  this.skippedCommands.push(command);
98
110
  this.skipReasons.set(command, 'disabled');
99
- this.commandTimings.set(command, Date.now() - startTime);
111
+ setTiming(Date.now() - startTime);
100
112
  visited.delete(command);
101
113
  return true;
102
114
  }
@@ -127,7 +139,7 @@ export class Orchestrator {
127
139
  process_tracking,
128
140
  kill_command,
129
141
  });
130
- this.commandTimings.set(command, Date.now() - startTime);
142
+ setTiming(Date.now() - startTime);
131
143
  visited.delete(command);
132
144
  return true;
133
145
  }
@@ -140,7 +152,7 @@ export class Orchestrator {
140
152
  this.logger.error(`Skipping ${command} due to failed dependency`);
141
153
  this.skippedCommands.push(command);
142
154
  this.skipReasons.set(command, 'failed_dependency');
143
- this.commandTimings.set(command, Date.now() - startTime);
155
+ setTiming(Date.now() - startTime);
144
156
  visited.delete(command);
145
157
  return false;
146
158
  }
@@ -154,7 +166,7 @@ export class Orchestrator {
154
166
  if (!urlAvailable) {
155
167
  this.skippedCommands.push(command);
156
168
  this.skipReasons.set(command, 'failed_dependency');
157
- this.commandTimings.set(command, Date.now() - startTime);
169
+ setTiming(Date.now() - startTime);
158
170
  visited.delete(command);
159
171
  return false;
160
172
  }
@@ -176,6 +188,7 @@ export class Orchestrator {
176
188
  let result = false;
177
189
  let commandOutput = '';
178
190
  let commandFailed = false;
191
+ let lastRunResult = null;
179
192
 
180
193
  for (let attempt = 1; attempt <= attempts; attempt++) {
181
194
  if (attempt > 1) {
@@ -185,7 +198,7 @@ export class Orchestrator {
185
198
  await new Promise((resolve) => setTimeout(resolve, 1000));
186
199
  }
187
200
 
188
- const { success, output } = await this.processManager.runCommand({
201
+ const runResult = await this.processManager.runCommand({
189
202
  cmd: attempt === 1 ? command : retry_command || command,
190
203
  logFile: log || logFile, // Prefer 'log' key over 'logFile' for backwards compatibility
191
204
  background,
@@ -193,7 +206,11 @@ export class Orchestrator {
193
206
  kill_command,
194
207
  isRetry: attempt > 1,
195
208
  env,
209
+ reportTime: this.metrics.includes('time'),
210
+ reportMemory: this.metrics.includes('memory'),
196
211
  });
212
+ lastRunResult = runResult;
213
+ const { success, output } = runResult;
197
214
  commandOutput = output;
198
215
  result = success;
199
216
 
@@ -239,7 +256,8 @@ export class Orchestrator {
239
256
  }
240
257
  }
241
258
 
242
- this.commandTimings.set(command, Date.now() - startTime);
259
+ const totalDurationMs = Date.now() - startTime;
260
+ setTiming(totalDurationMs, lastRunResult?.memoryKb ?? null);
243
261
  visited.delete(command);
244
262
  return result;
245
263
  }
@@ -269,6 +287,217 @@ export class Orchestrator {
269
287
  }
270
288
  }
271
289
 
290
+ writeJsonResults(hasFailures) {
291
+ const overallDurationMs =
292
+ this.metrics.includes('time') && this.startTime
293
+ ? Date.now() - this.startTime
294
+ : undefined;
295
+
296
+ const commands = [];
297
+ if (Array.isArray(this.config)) {
298
+ this.config.forEach(({ command }) => {
299
+ const timing = this.commandTimings.get(command);
300
+ const skipReason = this.skippedCommands.includes(command)
301
+ ? this.skipReasons.get(command) ?? null
302
+ : null;
303
+ const success =
304
+ !this.failedCommands.includes(command) &&
305
+ (skipReason === null ||
306
+ skipReason === 'disabled' ||
307
+ skipReason === 'optional_phase_not_requested' ||
308
+ skipReason === 'before_start_phase');
309
+ const entry = {
310
+ command,
311
+ success,
312
+ ...(timing?.durationMs != null && this.metrics.includes('time')
313
+ ? { durationMs: timing.durationMs }
314
+ : {}),
315
+ ...(this.metrics.includes('memory')
316
+ ? { memoryKb: timing?.memoryKb ?? null }
317
+ : {}),
318
+ ...(skipReason ? { skipReason } : {}),
319
+ };
320
+ commands.push(entry);
321
+ });
322
+ } else if (this.config.phases) {
323
+ this.config.phases.forEach((phase) => {
324
+ (phase.parallel || []).forEach(({ command }) => {
325
+ const timing = this.commandTimings.get(command);
326
+ const skipReason = this.skippedCommands.includes(command)
327
+ ? this.skipReasons.get(command) ?? null
328
+ : null;
329
+ const success =
330
+ !this.failedCommands.includes(command) &&
331
+ (skipReason === null ||
332
+ skipReason === 'disabled' ||
333
+ skipReason === 'optional_phase_not_requested' ||
334
+ skipReason === 'before_start_phase');
335
+ const entry = {
336
+ command,
337
+ phase: phase.name,
338
+ success,
339
+ ...(timing?.durationMs != null && this.metrics.includes('time')
340
+ ? { durationMs: timing.durationMs }
341
+ : {}),
342
+ ...(this.metrics.includes('memory')
343
+ ? { memoryKb: timing?.memoryKb ?? null }
344
+ : {}),
345
+ ...(skipReason ? { skipReason } : {}),
346
+ };
347
+ commands.push(entry);
348
+ });
349
+ });
350
+ }
351
+
352
+ const payload = {
353
+ success: !hasFailures,
354
+ timestamp: new Date().toISOString(),
355
+ ...(overallDurationMs != null ? { overallDurationMs } : {}),
356
+ commands,
357
+ ...(this.config.phases && this.phaseResults.length > 0
358
+ ? { phases: this.phaseResults }
359
+ : {}),
360
+ };
361
+
362
+ const json = JSON.stringify(payload, null, 2);
363
+ if (this.jsonResultsPath === '-') {
364
+ console.log(json);
365
+ } else {
366
+ const outPath = this.jsonResultsPath || './scripts-orchestrator-results.json';
367
+ fs.writeFileSync(outPath, json, 'utf8');
368
+ this.logger.verbose(`Wrote results to ${outPath}`);
369
+ }
370
+
371
+ if (this.htmlResultsPath != null) {
372
+ this.writeHtmlResults(payload);
373
+ }
374
+ }
375
+
376
+ formatMs(ms) {
377
+ if (ms == null || ms === 0) return '—';
378
+ if (ms < 1000) return `${ms}ms`;
379
+ const s = (ms / 1000).toFixed(1);
380
+ return `${s}s`;
381
+ }
382
+
383
+ writeHtmlResults(payload) {
384
+ const escapeHtml = (s) => {
385
+ if (s == null) return '';
386
+ return String(s)
387
+ .replace(/&/g, '&amp;')
388
+ .replace(/</g, '&lt;')
389
+ .replace(/>/g, '&gt;')
390
+ .replace(/"/g, '&quot;');
391
+ };
392
+ const { success, timestamp, overallDurationMs, commands = [], phases = [] } = payload;
393
+ const maxDuration = Math.max(0, ...commands.map((c) => c.durationMs || 0), ...phases.map((p) => p.durationMs || 0));
394
+ const maxMemory = Math.max(0, ...commands.map((c) => c.memoryKb || 0));
395
+
396
+ const row = (c) => {
397
+ const durationPct = maxDuration > 0 && c.durationMs != null ? (c.durationMs / maxDuration) * 100 : 0;
398
+ const memoryPct = maxMemory > 0 && c.memoryKb != null ? (c.memoryKb / maxMemory) * 100 : 0;
399
+ const statusClass = c.success ? 'ok' : 'fail';
400
+ const statusLabel = c.success ? 'OK' : (c.skipReason || 'Failed');
401
+ return `
402
+ <tr class="${statusClass}">
403
+ <td><code>${escapeHtml(c.command)}</code></td>
404
+ <td>${c.phase != null ? escapeHtml(c.phase) : '—'}</td>
405
+ <td><span class="badge ${statusClass}">${escapeHtml(statusLabel)}</span></td>
406
+ <td>${this.formatMs(c.durationMs)}</td>
407
+ <td>${c.memoryKb != null ? `${(c.memoryKb / 1024).toFixed(1)} MB` : '—'}</td>
408
+ <td class="bar-cell"><div class="bar" style="width:${durationPct}%"></div></td>
409
+ <td class="bar-cell"><div class="bar mem" style="width:${memoryPct}%"></div></td>
410
+ </tr>`;
411
+ };
412
+
413
+ const phaseRow = (p) => {
414
+ const durationPct = maxDuration > 0 && p.durationMs != null ? (p.durationMs / maxDuration) * 100 : 0;
415
+ const statusClass = p.success ? 'ok' : 'fail';
416
+ return `
417
+ <tr class="${statusClass}">
418
+ <td>${escapeHtml(p.name)}</td>
419
+ <td><span class="badge ${statusClass}">${p.success ? 'OK' : 'Failed'}</span></td>
420
+ <td>${this.formatMs(p.durationMs)}</td>
421
+ <td class="bar-cell"><div class="bar" style="width:${durationPct}%"></div></td>
422
+ </tr>`;
423
+ };
424
+
425
+ const html = `<!DOCTYPE html>
426
+ <html lang="en">
427
+ <head>
428
+ <meta charset="utf-8">
429
+ <title>Scripts Orchestrator Report</title>
430
+ <style>
431
+ * { box-sizing: border-box; }
432
+ body { font-family: system-ui, sans-serif; margin: 1rem 2rem; background: #1a1a1a; color: #e0e0e0; }
433
+ h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }
434
+ .summary { display: flex; gap: 1.5rem; flex-wrap: wrap; margin-bottom: 1.5rem; }
435
+ .summary .card { background: #2a2a2a; padding: 1rem 1.25rem; border-radius: 8px; min-width: 140px; }
436
+ .summary .card.success { border-left: 4px solid #22c55e; }
437
+ .summary .card.fail { border-left: 4px solid #ef4444; }
438
+ .summary .label { font-size: 0.75rem; text-transform: uppercase; color: #888; }
439
+ .summary .value { font-size: 1.25rem; font-weight: 600; }
440
+ section { margin-bottom: 1.5rem; }
441
+ section h2 { font-size: 1.1rem; color: #a0a0a0; margin-bottom: 0.5rem; }
442
+ table { width: 100%; border-collapse: collapse; background: #2a2a2a; border-radius: 8px; overflow: hidden; }
443
+ th, td { padding: 0.5rem 0.75rem; text-align: left; }
444
+ th { background: #333; color: #888; font-weight: 600; font-size: 0.8rem; }
445
+ tr.fail { background: rgba(239,68,68,0.08); }
446
+ .badge { padding: 0.2rem 0.5rem; border-radius: 4px; font-size: 0.8rem; }
447
+ .badge.ok { background: #22c55e; color: #0f0f0f; }
448
+ .badge.fail { background: #ef4444; color: #fff; }
449
+ .bar-cell { width: 120px; }
450
+ .bar { height: 8px; background: #3b82f6; border-radius: 4px; min-width: 2px; }
451
+ .bar.mem { background: #8b5cf6; }
452
+ code { font-size: 0.9em; background: #333; padding: 0.1rem 0.3rem; border-radius: 4px; }
453
+ </style>
454
+ </head>
455
+ <body>
456
+ <h1>Scripts Orchestrator Report</h1>
457
+ <div class="summary">
458
+ <div class="card ${success ? 'success' : 'fail'}">
459
+ <div class="label">Status</div>
460
+ <div class="value">${success ? 'Success' : 'Failed'}</div>
461
+ </div>
462
+ <div class="card">
463
+ <div class="label">Timestamp</div>
464
+ <div class="value" style="font-size:0.9rem">${escapeHtml(timestamp)}</div>
465
+ </div>
466
+ ${overallDurationMs != null ? `
467
+ <div class="card">
468
+ <div class="label">Total time</div>
469
+ <div class="value">${this.formatMs(overallDurationMs)}</div>
470
+ </div>` : ''}
471
+ </div>
472
+
473
+ ${phases.length > 0 ? `
474
+ <section>
475
+ <h2>Phases</h2>
476
+ <table>
477
+ <thead><tr><th>Phase</th><th>Status</th><th>Duration</th><th></th></tr></thead>
478
+ <tbody>${phases.map(phaseRow).join('')}</tbody>
479
+ </table>
480
+ </section>` : ''}
481
+
482
+ <section>
483
+ <h2>Commands</h2>
484
+ <table>
485
+ <thead><tr><th>Command</th><th>Phase</th><th>Status</th><th>Duration</th><th>Memory</th><th>Time</th><th>Memory</th></tr></thead>
486
+ <tbody>${commands.map(row).join('')}</tbody>
487
+ </table>
488
+ </section>
489
+ </body>
490
+ </html>`;
491
+
492
+ if (this.htmlResultsPath === '-') {
493
+ console.log(html);
494
+ } else {
495
+ const outPath = this.htmlResultsPath || './scripts-orchestrator-results.html';
496
+ fs.writeFileSync(outPath, html, 'utf8');
497
+ this.logger.verbose(`Wrote HTML report to ${outPath}`);
498
+ }
499
+ }
500
+
272
501
  async run() {
273
502
  this.startTime = Date.now();
274
503
  try {
@@ -328,7 +557,7 @@ export class Orchestrator {
328
557
  phase.parallel.forEach(({ command }) => {
329
558
  this.skippedCommands.push(command);
330
559
  this.skipReasons.set(command, 'before_start_phase');
331
- this.commandTimings.set(command, 0);
560
+ this.commandTimings.set(command, { durationMs: 0, memoryKb: null });
332
561
  });
333
562
  continue;
334
563
  }
@@ -347,7 +576,7 @@ export class Orchestrator {
347
576
  phase.parallel.forEach(({ command }) => {
348
577
  this.skippedCommands.push(command);
349
578
  this.skipReasons.set(command, 'optional_phase_not_requested');
350
- this.commandTimings.set(command, 0);
579
+ this.commandTimings.set(command, { durationMs: 0, memoryKb: null });
351
580
  });
352
581
  continue;
353
582
  }
@@ -357,7 +586,7 @@ export class Orchestrator {
357
586
  phase.parallel.forEach(({ command }) => {
358
587
  this.skippedCommands.push(command);
359
588
  this.skipReasons.set(command, 'after_phase_failure');
360
- this.commandTimings.set(command, 0);
589
+ this.commandTimings.set(command, { durationMs: 0, memoryKb: null });
361
590
  });
362
591
  continue;
363
592
  }
@@ -385,9 +614,16 @@ export class Orchestrator {
385
614
  }
386
615
 
387
616
  const phaseHasFailures = results.some((result) => !result);
388
- const phaseDurationStr = `(${this.formatDuration(
389
- Date.now() - phaseStartTime,
390
- )})`;
617
+ const phaseDurationMs = Date.now() - phaseStartTime;
618
+ const phaseDurationStr = this.metrics.includes('time')
619
+ ? `(${this.formatDuration(phaseDurationMs)})`
620
+ : '';
621
+
622
+ this.phaseResults.push({
623
+ name: phase.name,
624
+ success: !phaseHasFailures,
625
+ durationMs: this.metrics.includes('time') ? phaseDurationMs : undefined,
626
+ });
391
627
 
392
628
  if (phaseHasFailures) {
393
629
  hasFailures = true;
@@ -450,8 +686,8 @@ export class Orchestrator {
450
686
  this.logger.error(`Cleanup failed: ${error.message}`);
451
687
  }
452
688
 
453
- // Log overall time after cleanup has finished
454
- if (this.startTime) {
689
+ // Log overall time after cleanup has finished (only when metrics include time)
690
+ if (this.startTime && this.metrics.includes('time')) {
455
691
  const overallDuration = Date.now() - this.startTime;
456
692
  this.logger.printMessage(() =>
457
693
  console.log(
@@ -462,6 +698,11 @@ export class Orchestrator {
462
698
  );
463
699
  }
464
700
 
701
+ // Write JSON results if requested
702
+ if (this.jsonResultsPath != null) {
703
+ this.writeJsonResults(hasFailures);
704
+ }
705
+
465
706
  // Update git cache on successful execution
466
707
  if (!hasFailures) {
467
708
  await this.gitCache.updateCache();
@@ -55,6 +55,32 @@ export class ProcessManager {
55
55
  });
56
56
  }
57
57
 
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
+
58
84
  async runCommand({
59
85
  cmd,
60
86
  logFile,
@@ -63,6 +89,8 @@ export class ProcessManager {
63
89
  kill_command = null,
64
90
  isRetry = false,
65
91
  env = null,
92
+ reportTime = false,
93
+ reportMemory = false,
66
94
  }) {
67
95
  const baseDir = this.logFolder
68
96
  ? path.resolve(this.logFolder)
@@ -88,11 +116,17 @@ export class ProcessManager {
88
116
  }
89
117
  } catch (error) {
90
118
  this.logger.error(`Failed to setup log file: ${error.message}`);
91
- return Promise.resolve({ success: false, output: '' });
119
+ return Promise.resolve({
120
+ success: false,
121
+ output: '',
122
+ durationMs: 0,
123
+ memoryKb: null,
124
+ });
92
125
  }
93
126
 
94
127
  return new Promise((resolve) => {
95
128
  const startTime = Date.now();
129
+ let timeOutputPath = null;
96
130
  // Build command with environment variables if provided
97
131
  let fullCommand = `npm run ${cmd}`;
98
132
  if (env && Object.keys(env).length > 0) {
@@ -101,6 +135,14 @@ export class ProcessManager {
101
135
  .join(' ');
102
136
  fullCommand = `${envStr} npm run ${cmd}`;
103
137
  }
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
+ }
104
146
 
105
147
  this.logger.startTask(cmd, fullCommand);
106
148
 
@@ -126,8 +168,12 @@ export class ProcessManager {
126
168
  processInstance.on('error', (error) => {
127
169
  this.logger.stopTask(cmd);
128
170
  this.logger.error(`Failed to start process: ${error.message}`);
129
- //this.logger.verbose(`Process error details: ${JSON.stringify(error, null, 2)}`);
130
- resolve({ success: false, output: '' });
171
+ resolve({
172
+ success: false,
173
+ output: '',
174
+ durationMs: Date.now() - startTime,
175
+ memoryKb: null,
176
+ });
131
177
  });
132
178
 
133
179
  if (background) {
@@ -189,7 +235,12 @@ export class ProcessManager {
189
235
  `Failed to read log file: ${error.message}`,
190
236
  );
191
237
  }
192
- return { success: false, output };
238
+ return {
239
+ success: false,
240
+ output,
241
+ durationMs: Date.now() - startTime,
242
+ memoryKb: null,
243
+ };
193
244
  }
194
245
 
195
246
  this.logger.verbose(
@@ -216,7 +267,12 @@ export class ProcessManager {
216
267
  `Failed to read log file: ${error.message}`,
217
268
  );
218
269
  }
219
- return { success: false, output };
270
+ return {
271
+ success: false,
272
+ output,
273
+ durationMs: Date.now() - startTime,
274
+ memoryKb: null,
275
+ };
220
276
  }
221
277
 
222
278
  this.backgroundProcesses.push(processGroupId);
@@ -236,7 +292,12 @@ export class ProcessManager {
236
292
  this.logger.verbose(
237
293
  `Background process started: npm run ${cmd} (PGID: ${processGroupId})`,
238
294
  );
239
- return { success: true, output: '' };
295
+ return {
296
+ success: true,
297
+ output: '',
298
+ durationMs: Date.now() - startTime,
299
+ memoryKb: null,
300
+ };
240
301
  } catch (error) {
241
302
  if (attempt === maxAttempts) {
242
303
  this.logger.error(
@@ -245,7 +306,12 @@ export class ProcessManager {
245
306
  this.logger.verbose(
246
307
  `Final verification attempt failed: ${error.message}`,
247
308
  );
248
- return { success: false, output: '' };
309
+ return {
310
+ success: false,
311
+ output: '',
312
+ durationMs: Date.now() - startTime,
313
+ memoryKb: null,
314
+ };
249
315
  }
250
316
  this.logger.verbose(
251
317
  `Verification attempt ${attempt} failed: ${error.message}`,
@@ -258,7 +324,12 @@ export class ProcessManager {
258
324
  );
259
325
  }
260
326
  }
261
- return { success: false, output: '' };
327
+ return {
328
+ success: false,
329
+ output: '',
330
+ durationMs: Date.now() - startTime,
331
+ memoryKb: null,
332
+ };
262
333
  };
263
334
 
264
335
  verifyProcess().then(resolve);
@@ -293,26 +364,50 @@ export class ProcessManager {
293
364
 
294
365
  this.logger.stopTask(cmd);
295
366
 
296
- const duration = Date.now() - startTime;
297
- const durationStr = ` (${this.formatDuration(duration)})`;
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
+ }
298
379
 
299
380
  if (code !== 0) {
300
381
  this.logger.error(
301
382
  `Failed: npm run ${cmd} ❌${durationStr} (exit code: ${code})`,
302
383
  );
303
384
  this.logger.verbose(`Process output: ${output}`);
304
- resolve({ success: false, output });
385
+ resolve({
386
+ success: false,
387
+ output,
388
+ durationMs,
389
+ memoryKb,
390
+ });
305
391
  } else {
306
392
  this.logger.success(`Completed: npm run ${cmd} ✅${durationStr}`);
307
- resolve({ success: true, output });
393
+ resolve({
394
+ success: true,
395
+ output,
396
+ durationMs,
397
+ memoryKb,
398
+ });
308
399
  }
309
400
  });
310
401
  }
311
402
  } catch (error) {
312
403
  this.logger.stopTask(cmd);
313
404
  this.logger.error(`Failed to spawn process: ${error.message}`);
314
- //this.logger.verbose(`Spawn error details: ${JSON.stringify(error, null, 2)}`);
315
- resolve({ success: false, output: '' });
405
+ resolve({
406
+ success: false,
407
+ output: '',
408
+ durationMs: Date.now() - startTime,
409
+ memoryKb: null,
410
+ });
316
411
  }
317
412
  });
318
413
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scripts-orchestrator",
3
- "version": "2.12.0",
3
+ "version": "2.13.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",
@@ -1,4 +1,10 @@
1
1
  export default {
2
+ // Optional: metrics to report (time, memory). CLI --metrics overrides.
3
+ // metrics: ['time'],
4
+ // Optional: path for JSON results, or '-' for stdout. CLI --json-results overrides.
5
+ // json_results: './scripts-orchestrator-results.json',
6
+ // Optional: path for HTML report. CLI --html-results overrides.
7
+ // html_results: './scripts-orchestrator-results.html',
2
8
  phases: [
3
9
  {
4
10
  name: 'build',