scripts-orchestrator 2.1.0 → 2.4.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 +0 -1
- package/lib/health-check.js +35 -12
- package/lib/orchestrator.js +10 -0
- package/lib/process-manager.js +239 -103
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -192,7 +192,6 @@ See [versions](./docs/versions.md)
|
|
|
192
192
|
## Roadmap
|
|
193
193
|
- Better UX to indicate what is happening
|
|
194
194
|
- Tests to avoid regression
|
|
195
|
-
- Retry should append to the log file
|
|
196
195
|
- Run any shell command rather than assume the command is specified in package.json (? tentative)
|
|
197
196
|
|
|
198
197
|
|
package/lib/health-check.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
2
1
|
import { log } from './logger.js';
|
|
3
2
|
|
|
4
3
|
export class HealthCheck {
|
|
@@ -6,22 +5,46 @@ export class HealthCheck {
|
|
|
6
5
|
this.logger = log;
|
|
7
6
|
}
|
|
8
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Static method to make a simple HTTP/HTTPS request
|
|
10
|
+
* @param {string} url - The URL to check
|
|
11
|
+
* @param {number} timeout - Request timeout in milliseconds (default: 5000)
|
|
12
|
+
* @returns {Promise<{statusCode: number|null, success: boolean, error?: string}>}
|
|
13
|
+
*/
|
|
14
|
+
static async makeHttpRequest(url, timeout = 5000) {
|
|
15
|
+
try {
|
|
16
|
+
// Use Node.js http/https module instead of curl for cross-platform compatibility
|
|
17
|
+
const urlObj = new URL(url);
|
|
18
|
+
const isHttps = urlObj.protocol === 'https:';
|
|
19
|
+
const httpModule = isHttps ? await import('https') : await import('http');
|
|
20
|
+
|
|
21
|
+
return await new Promise((resolve) => {
|
|
22
|
+
const req = httpModule.default.get(url, (res) => {
|
|
23
|
+
resolve({ statusCode: res.statusCode, success: true });
|
|
24
|
+
res.destroy(); // Close the response stream
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
req.on('error', (error) => {
|
|
28
|
+
resolve({ statusCode: null, success: false, error: error.message });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
req.setTimeout(timeout, () => {
|
|
32
|
+
req.destroy();
|
|
33
|
+
resolve({ statusCode: null, success: false, error: 'Timeout' });
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
} catch (error) {
|
|
37
|
+
return { statusCode: null, success: false, error: error.message };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
9
41
|
async waitForUrl({url, maxAttempts = 20, interval = 2000, silent=false}) {
|
|
10
42
|
!silent && this.logger.info(`Waiting for ${url} to be available...`);
|
|
11
43
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
12
44
|
try {
|
|
13
|
-
const result = await
|
|
14
|
-
const curl = spawn('curl', ['-s', '-o', '/dev/null', '-w', '%{http_code}', url]);
|
|
15
|
-
let output = '';
|
|
16
|
-
curl.stdout.on('data', (data) => {
|
|
17
|
-
output += data.toString();
|
|
18
|
-
});
|
|
19
|
-
curl.on('close', (code) => {
|
|
20
|
-
resolve({ code, output });
|
|
21
|
-
});
|
|
22
|
-
});
|
|
45
|
+
const result = await HealthCheck.makeHttpRequest(url, 5000);
|
|
23
46
|
|
|
24
|
-
if (result.
|
|
47
|
+
if (result.success && result.statusCode === 200) {
|
|
25
48
|
!silent && this.logger.success(`${url} is available`);
|
|
26
49
|
return true;
|
|
27
50
|
}
|
package/lib/orchestrator.js
CHANGED
|
@@ -179,6 +179,16 @@ export class Orchestrator {
|
|
|
179
179
|
|
|
180
180
|
if (commandFailed) {
|
|
181
181
|
this.failedCommands.push(command);
|
|
182
|
+
|
|
183
|
+
// Cleanup any background processes for this failed command
|
|
184
|
+
if (background) {
|
|
185
|
+
this.logger.warn(`Command ${command} failed after all attempts. Cleaning up background processes.`);
|
|
186
|
+
try {
|
|
187
|
+
await this.processManager.cleanupCommand(command);
|
|
188
|
+
} catch (cleanupError) {
|
|
189
|
+
this.logger.error(`Failed to cleanup processes for ${command}: ${cleanupError.message}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
182
192
|
}
|
|
183
193
|
|
|
184
194
|
this.commandTimings.set(command, Date.now() - startTime);
|
package/lib/process-manager.js
CHANGED
|
@@ -2,6 +2,7 @@ import { spawn } from 'child_process';
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { log } from './logger.js';
|
|
5
|
+
import { HealthCheck } from './health-check.js';
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
export class ProcessManager {
|
|
@@ -75,6 +76,16 @@ export class ProcessManager {
|
|
|
75
76
|
const processGroupId = processInstance.pid;
|
|
76
77
|
this.logger.verbose(`Background process spawned with PID: ${processGroupId}`);
|
|
77
78
|
|
|
79
|
+
// Track process exit for background processes
|
|
80
|
+
let processExited = false;
|
|
81
|
+
let processExitCode = null;
|
|
82
|
+
|
|
83
|
+
processInstance.on('exit', (code, signal) => {
|
|
84
|
+
processExited = true;
|
|
85
|
+
processExitCode = code;
|
|
86
|
+
this.logger.verbose(`Background process ${cmd} (PID: ${processGroupId}) exited with code: ${code}, signal: ${signal}`);
|
|
87
|
+
});
|
|
88
|
+
|
|
78
89
|
processInstance.stdout.on('data', (data) => {
|
|
79
90
|
try {
|
|
80
91
|
fs.appendFileSync(LOG_FILE, data.toString());
|
|
@@ -97,10 +108,39 @@ export class ProcessManager {
|
|
|
97
108
|
|
|
98
109
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
99
110
|
try {
|
|
111
|
+
// First check if the process has already exited with an error
|
|
112
|
+
if (processExited && processExitCode !== 0) {
|
|
113
|
+
this.logger.error(`Background process ${cmd} exited with code ${processExitCode}`);
|
|
114
|
+
let output = '';
|
|
115
|
+
try {
|
|
116
|
+
output = fs.readFileSync(LOG_FILE, 'utf8');
|
|
117
|
+
this.logger.verbose(`Process output: ${output}`);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
this.logger.error(`Failed to read log file: ${error.message}`);
|
|
120
|
+
}
|
|
121
|
+
return { success: false, output };
|
|
122
|
+
}
|
|
123
|
+
|
|
100
124
|
this.logger.verbose(`Verifying process ${processGroupId} (attempt ${attempt}/${maxAttempts})`);
|
|
101
125
|
process.kill(processGroupId, 0);
|
|
102
126
|
this.logger.verbose(`Process ${processGroupId} is running`);
|
|
103
127
|
|
|
128
|
+
// Wait a bit more to ensure the process doesn't exit immediately after verification
|
|
129
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
130
|
+
|
|
131
|
+
// Check again if the process exited during our wait
|
|
132
|
+
if (processExited && processExitCode !== 0) {
|
|
133
|
+
this.logger.error(`Background process ${cmd} exited with code ${processExitCode} shortly after starting`);
|
|
134
|
+
let output = '';
|
|
135
|
+
try {
|
|
136
|
+
output = fs.readFileSync(LOG_FILE, 'utf8');
|
|
137
|
+
this.logger.verbose(`Process output: ${output}`);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
this.logger.error(`Failed to read log file: ${error.message}`);
|
|
140
|
+
}
|
|
141
|
+
return { success: false, output };
|
|
142
|
+
}
|
|
143
|
+
|
|
104
144
|
this.backgroundProcesses.push(processGroupId);
|
|
105
145
|
this.backgroundProcessesDetails.push({
|
|
106
146
|
command: cmd,
|
|
@@ -218,139 +258,235 @@ export class ProcessManager {
|
|
|
218
258
|
|
|
219
259
|
const killPromises = this.backgroundProcessesDetails.map(
|
|
220
260
|
async ({ command, pgid, url, startedByScript, kill_command }) => {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
);
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
261
|
+
await this.cleanupProcess({ command, pgid, url, startedByScript, kill_command });
|
|
262
|
+
},
|
|
263
|
+
);
|
|
227
264
|
|
|
228
|
-
|
|
265
|
+
await Promise.all(killPromises);
|
|
266
|
+
this.backgroundProcesses = [];
|
|
267
|
+
this.backgroundProcessesDetails = [];
|
|
268
|
+
}
|
|
229
269
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
270
|
+
async cleanupCommand(commandName) {
|
|
271
|
+
this.logger.info(`\nCleaning up processes for command: ${commandName}`);
|
|
272
|
+
|
|
273
|
+
// Find processes for this specific command
|
|
274
|
+
const commandProcesses = this.backgroundProcessesDetails.filter(
|
|
275
|
+
({ command }) => command === commandName
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
if (commandProcesses.length === 0) {
|
|
279
|
+
this.logger.verbose(`- No background processes found for command: ${commandName}`);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
this.logger.verbose(`- Found ${commandProcesses.length} background processes for command: ${commandName}`);
|
|
284
|
+
|
|
285
|
+
const killPromises = commandProcesses.map(
|
|
286
|
+
async ({ command, pgid, url, startedByScript, kill_command }) => {
|
|
287
|
+
await this.cleanupProcess({ command, pgid, url, startedByScript, kill_command });
|
|
288
|
+
}
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
await Promise.all(killPromises);
|
|
292
|
+
|
|
293
|
+
// Remove the cleaned up processes from our tracking arrays
|
|
294
|
+
this.backgroundProcesses = this.backgroundProcesses.filter(pgid =>
|
|
295
|
+
!commandProcesses.some(proc => proc.pgid === pgid)
|
|
296
|
+
);
|
|
297
|
+
this.backgroundProcessesDetails = this.backgroundProcessesDetails.filter(
|
|
298
|
+
({ command }) => command !== commandName
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async cleanupProcess({ command, pgid, url, startedByScript, kill_command }) {
|
|
303
|
+
if (!startedByScript) {
|
|
304
|
+
this.logger.verbose(
|
|
305
|
+
`- Skipping cleanup for ${command} (${url}) as it was not started by this script`,
|
|
306
|
+
);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
this.logger.verbose(`- Processing cleanup for ${command} (kill_command: ${kill_command})`);
|
|
311
|
+
|
|
312
|
+
// Try custom kill command first if specified
|
|
313
|
+
if (kill_command) {
|
|
314
|
+
try {
|
|
315
|
+
this.logger.verbose(`- Using custom kill command: npm run ${kill_command}`);
|
|
316
|
+
const result = await this.runCommand({ cmd: kill_command, logFile: null, background: false });
|
|
317
|
+
if (result.success) {
|
|
318
|
+
this.logger.verbose(`- Successfully killed ${command} using custom command`);
|
|
319
|
+
return;
|
|
244
320
|
} else {
|
|
245
|
-
this.logger.verbose(
|
|
321
|
+
this.logger.verbose('- Custom kill command failed, falling back to process signals');
|
|
246
322
|
}
|
|
323
|
+
} catch (error) {
|
|
324
|
+
this.logger.verbose(`- Custom kill command error: ${error.message}, falling back`);
|
|
325
|
+
}
|
|
326
|
+
} else {
|
|
327
|
+
this.logger.verbose(`- No kill_command specified for ${command}, using process signals`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
// First try to kill the process group
|
|
332
|
+
try {
|
|
333
|
+
process.kill(pgid, 0);
|
|
334
|
+
} catch (error) {
|
|
335
|
+
this.logger.verbose(
|
|
336
|
+
`- Process ${command} (PGID: ${pgid}) already terminated`,
|
|
337
|
+
);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
247
340
|
|
|
341
|
+
// Cross-platform process termination
|
|
342
|
+
const isWindows = process.platform === 'win32';
|
|
343
|
+
|
|
344
|
+
if (isWindows) {
|
|
345
|
+
// Windows: use taskkill to terminate process tree
|
|
248
346
|
try {
|
|
249
|
-
|
|
347
|
+
const killProcess = spawn('taskkill', ['/F', '/T', '/PID', pgid.toString()]);
|
|
348
|
+
await new Promise((resolve) => {
|
|
349
|
+
killProcess.on('close', resolve);
|
|
350
|
+
});
|
|
351
|
+
this.logger.verbose(`- Terminated background process: ${command} (PID: ${pgid})`);
|
|
352
|
+
return;
|
|
353
|
+
} catch (killError) {
|
|
354
|
+
this.logger.verbose(`- Failed to use taskkill, falling back to process.kill: ${killError.message}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Unix/Linux/macOS or Windows fallback: Try SIGTERM first
|
|
359
|
+
process.kill(pgid, 'SIGTERM');
|
|
360
|
+
|
|
361
|
+
await new Promise((resolve, reject) => {
|
|
362
|
+
const timeout = setTimeout(() => {
|
|
363
|
+
clearInterval(checkInterval);
|
|
364
|
+
reject(new Error('Process termination timeout'));
|
|
365
|
+
}, 5000);
|
|
366
|
+
|
|
367
|
+
const checkInterval = setInterval(() => {
|
|
250
368
|
try {
|
|
251
369
|
process.kill(pgid, 0);
|
|
252
370
|
} catch (error) {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
);
|
|
256
|
-
return;
|
|
371
|
+
clearInterval(checkInterval);
|
|
372
|
+
clearTimeout(timeout);
|
|
373
|
+
resolve();
|
|
257
374
|
}
|
|
375
|
+
}, 100);
|
|
376
|
+
});
|
|
377
|
+
this.logger.verbose(
|
|
378
|
+
`- Terminated background process: ${command} (PGID: ${pgid})`,
|
|
379
|
+
);
|
|
380
|
+
} catch (error) {
|
|
381
|
+
this.logger.verbose(`- Failed to terminate process group: ${error.message}`);
|
|
382
|
+
}
|
|
258
383
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
clearInterval(checkInterval);
|
|
273
|
-
clearTimeout(timeout);
|
|
274
|
-
resolve();
|
|
275
|
-
}
|
|
276
|
-
}, 100);
|
|
277
|
-
});
|
|
278
|
-
this.logger.verbose(
|
|
279
|
-
`- Terminated background process: ${command} (PGID: ${pgid})`,
|
|
280
|
-
);
|
|
281
|
-
} catch (error) {
|
|
282
|
-
this.logger.verbose(`- Failed to terminate process group: ${error.message}`);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Check if the URL is still responding after termination attempt
|
|
286
|
-
if (url) {
|
|
384
|
+
// Check if the URL is still responding after termination attempt
|
|
385
|
+
if (url) {
|
|
386
|
+
try {
|
|
387
|
+
const urlObj = new URL(url);
|
|
388
|
+
const port = urlObj.port || (urlObj.protocol === 'https:' ? '443' : '80');
|
|
389
|
+
|
|
390
|
+
// Use shared HTTP utility for cross-platform compatibility
|
|
391
|
+
const urlResult = await HealthCheck.makeHttpRequest(url, 2000);
|
|
392
|
+
|
|
393
|
+
if (urlResult.success && urlResult.statusCode === 200) {
|
|
394
|
+
this.logger.verbose(`- URL ${url} is still responding after termination, finding process on port ${port}`);
|
|
395
|
+
|
|
396
|
+
// Find and kill process using the port - cross-platform approach
|
|
287
397
|
try {
|
|
288
|
-
const
|
|
289
|
-
|
|
398
|
+
const isWindows = process.platform === 'win32';
|
|
399
|
+
let findPortCmd, findPortArgs;
|
|
290
400
|
|
|
291
|
-
|
|
401
|
+
if (isWindows) {
|
|
402
|
+
// Windows: use netstat
|
|
403
|
+
findPortCmd = 'netstat';
|
|
404
|
+
findPortArgs = ['-ano'];
|
|
405
|
+
} else {
|
|
406
|
+
// Unix/Linux/macOS: use lsof
|
|
407
|
+
findPortCmd = 'lsof';
|
|
408
|
+
findPortArgs = ['-i', `:${port}`, '-t'];
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const findProcess = spawn(findPortCmd, findPortArgs);
|
|
292
412
|
const result = await new Promise((resolve) => {
|
|
293
413
|
let output = '';
|
|
294
|
-
|
|
414
|
+
findProcess.stdout.on('data', (data) => {
|
|
295
415
|
output += data.toString();
|
|
296
416
|
});
|
|
297
|
-
|
|
417
|
+
findProcess.on('close', (code) => {
|
|
298
418
|
resolve({ code, output });
|
|
299
419
|
});
|
|
300
420
|
});
|
|
301
421
|
|
|
302
|
-
if (result.code === 0 && result.output
|
|
303
|
-
|
|
422
|
+
if (result.code === 0 && result.output.trim()) {
|
|
423
|
+
let pids = [];
|
|
304
424
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
const
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
resolve({ code, output });
|
|
315
|
-
});
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
if (result.code === 0 && result.output.trim()) {
|
|
319
|
-
const pids = result.output.trim().split('\n');
|
|
320
|
-
for (const pid of pids) {
|
|
321
|
-
try {
|
|
322
|
-
process.kill(parseInt(pid), 'SIGKILL');
|
|
323
|
-
this.logger.verbose(`- Killed process (PID: ${pid}) using port ${port}`);
|
|
324
|
-
} catch (killError) {
|
|
325
|
-
if (killError.code !== 'ESRCH') {
|
|
326
|
-
this.logger.error(`- Failed to kill process (PID: ${pid}): ${killError.message}`);
|
|
327
|
-
}
|
|
425
|
+
if (isWindows) {
|
|
426
|
+
// Parse netstat output to find PIDs for the specific port
|
|
427
|
+
const lines = result.output.split('\n');
|
|
428
|
+
for (const line of lines) {
|
|
429
|
+
if (line.includes(`:${port} `) && line.includes('LISTENING')) {
|
|
430
|
+
const parts = line.trim().split(/\s+/);
|
|
431
|
+
const pid = parts[parts.length - 1];
|
|
432
|
+
if (pid && !isNaN(pid)) {
|
|
433
|
+
pids.push(pid);
|
|
328
434
|
}
|
|
329
435
|
}
|
|
330
436
|
}
|
|
331
|
-
}
|
|
332
|
-
|
|
437
|
+
} else {
|
|
438
|
+
// lsof output is already just PIDs
|
|
439
|
+
pids = result.output.trim().split('\n');
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
for (const pid of pids) {
|
|
443
|
+
try {
|
|
444
|
+
if (isWindows) {
|
|
445
|
+
// Windows: use taskkill
|
|
446
|
+
const killProcess = spawn('taskkill', ['/F', '/PID', pid]);
|
|
447
|
+
await new Promise((resolve) => {
|
|
448
|
+
killProcess.on('close', resolve);
|
|
449
|
+
});
|
|
450
|
+
} else {
|
|
451
|
+
// Unix/Linux/macOS: use process.kill
|
|
452
|
+
process.kill(parseInt(pid), 'SIGKILL');
|
|
453
|
+
}
|
|
454
|
+
this.logger.verbose(`- Killed process (PID: ${pid}) using port ${port}`);
|
|
455
|
+
} catch (killError) {
|
|
456
|
+
if (killError.code !== 'ESRCH') {
|
|
457
|
+
this.logger.error(`- Failed to kill process (PID: ${pid}): ${killError.message}`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
333
460
|
}
|
|
334
461
|
}
|
|
335
|
-
} catch (
|
|
336
|
-
this.logger.
|
|
462
|
+
} catch (portError) {
|
|
463
|
+
this.logger.error(`- Failed to find process using port ${port}: ${portError.message}`);
|
|
337
464
|
}
|
|
338
465
|
}
|
|
466
|
+
} catch (error) {
|
|
467
|
+
this.logger.verbose(`- URL check failed: ${error.message}`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
339
470
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
471
|
+
// Final attempt to kill the process group
|
|
472
|
+
try {
|
|
473
|
+
const isWindows = process.platform === 'win32';
|
|
474
|
+
|
|
475
|
+
if (isWindows) {
|
|
476
|
+
// Windows: force kill with taskkill
|
|
477
|
+
const killProcess = spawn('taskkill', ['/F', '/T', '/PID', pgid.toString()]);
|
|
478
|
+
await new Promise((resolve) => {
|
|
479
|
+
killProcess.on('close', resolve);
|
|
480
|
+
});
|
|
481
|
+
} else {
|
|
482
|
+
// Unix/Linux/macOS: use SIGKILL
|
|
483
|
+
process.kill(pgid, 'SIGKILL');
|
|
484
|
+
}
|
|
485
|
+
} catch (error) {
|
|
486
|
+
if (error.code !== 'ESRCH') {
|
|
487
|
+
this.logger.error(`- Failed to kill process group: ${error.message}`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
354
490
|
}
|
|
355
491
|
}
|
|
356
492
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scripts-orchestrator",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.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",
|