scripts-orchestrator 2.15.1 → 3.0.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 +41 -1
- package/lib/orchestrator.js +34 -1
- package/lib/orchestrator.prefix.test.js +60 -0
- package/lib/process-manager.js +21 -9
- package/lib/process-manager.test.js +58 -0
- package/package.json +1 -1
- package/scripts-orchestrator.config.js +4 -0
package/README.md
CHANGED
|
@@ -48,12 +48,14 @@ Create a configuration file (default: `scripts-orchestrator.config.js`) that def
|
|
|
48
48
|
|
|
49
49
|
```javascript
|
|
50
50
|
{
|
|
51
|
-
command: 'command_name', // The
|
|
51
|
+
command: 'command_name', // The command to run (see "Command prefix" below)
|
|
52
52
|
description: 'Description', // Optional description
|
|
53
53
|
status: 'enabled', // 'enabled' or 'disabled'
|
|
54
54
|
attempts: 1, // Number of retry attempts
|
|
55
55
|
dependencies: [], // Array of dependent commands
|
|
56
56
|
background: false, // Whether to run in background
|
|
57
|
+
shell: false, // true => run `command` verbatim as a shell command (no prefix)
|
|
58
|
+
prefix: 'npm run', // Optional per-command prefix override ('' to disable)
|
|
57
59
|
env: { // Optional environment variables
|
|
58
60
|
PORT: 3000,
|
|
59
61
|
NODE_ENV: 'production'
|
|
@@ -70,6 +72,44 @@ Create a configuration file (default: `scripts-orchestrator.config.js`) that def
|
|
|
70
72
|
}
|
|
71
73
|
```
|
|
72
74
|
|
|
75
|
+
### Command prefix (`npm run` is optional)
|
|
76
|
+
|
|
77
|
+
By default every `command` is run as an npm script — the orchestrator prepends `npm run`,
|
|
78
|
+
so `command: 'build'` executes `npm run build`. This prefix is configurable:
|
|
79
|
+
|
|
80
|
+
- **Global default** — set `command_prefix` at the top level of the config. Use it to point at a
|
|
81
|
+
different runner (`'pnpm run'`, `'yarn'`) or to disable prefixing entirely so commands run as
|
|
82
|
+
regular shell commands:
|
|
83
|
+
|
|
84
|
+
```javascript
|
|
85
|
+
export default {
|
|
86
|
+
command_prefix: '', // '' / false / null => run commands verbatim (plain shell)
|
|
87
|
+
phases: [
|
|
88
|
+
{ name: 'checks', parallel: [
|
|
89
|
+
{ command: 'eslint . --max-warnings 0' }, // runs as-is, supports args/pipes/&&
|
|
90
|
+
{ command: './scripts/verify.sh' },
|
|
91
|
+
]},
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
- **Per command** — `shell: true` forces a single command to run verbatim as a shell command
|
|
97
|
+
(ignoring any global prefix), and `prefix: '...'` overrides the prefix for just that command:
|
|
98
|
+
|
|
99
|
+
```javascript
|
|
100
|
+
{
|
|
101
|
+
phases: [{ name: 'mixed', parallel: [
|
|
102
|
+
{ command: 'build' }, // -> npm run build (global default)
|
|
103
|
+
{ command: 'docker compose up -d', shell: true }, // raw shell command
|
|
104
|
+
{ command: 'release', prefix: 'yarn' }, // -> yarn release
|
|
105
|
+
]}],
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Precedence per command: `shell: true` (raw) → per-command `prefix` → global `command_prefix`
|
|
110
|
+
→ the built-in `npm run` default. Existing configs are unaffected — omitting all of these keeps
|
|
111
|
+
the original `npm run` behaviour.
|
|
112
|
+
|
|
73
113
|
### Phase Configuration
|
|
74
114
|
|
|
75
115
|
When using the phases format, each phase can have the following properties:
|
package/lib/orchestrator.js
CHANGED
|
@@ -27,6 +27,12 @@ export class Orchestrator {
|
|
|
27
27
|
this.sequential = sequential;
|
|
28
28
|
this.force = force;
|
|
29
29
|
this.metrics = Array.isArray(metrics) ? metrics : [];
|
|
30
|
+
// Global command prefix. Defaults to 'npm run' so existing configs keep working.
|
|
31
|
+
// Set `command_prefix` to '' / false / null in the config to run commands verbatim
|
|
32
|
+
// as regular shell commands. Per-command `shell: true` or `prefix` overrides this.
|
|
33
|
+
this.commandPrefix = Object.prototype.hasOwnProperty.call(config ?? {}, 'command_prefix')
|
|
34
|
+
? this._normalizePrefix(config.command_prefix)
|
|
35
|
+
: 'npm run';
|
|
30
36
|
this.jsonResultsPath = jsonResultsPath ?? null;
|
|
31
37
|
this.htmlResultsPath = htmlResultsPath ?? null;
|
|
32
38
|
this.processManager = processManager;
|
|
@@ -263,6 +269,29 @@ export class Orchestrator {
|
|
|
263
269
|
}
|
|
264
270
|
}
|
|
265
271
|
|
|
272
|
+
// Normalize a prefix value into a clean string. false/null/'' all mean "no prefix"
|
|
273
|
+
// (run the command verbatim); any string is trimmed.
|
|
274
|
+
_normalizePrefix(value) {
|
|
275
|
+
if (value === false || value === null || value === undefined) return '';
|
|
276
|
+
return String(value).trim();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Resolve the effective prefix for a single command.
|
|
280
|
+
// Precedence: per-command `shell: true` (raw) > per-command `prefix` > global commandPrefix.
|
|
281
|
+
_resolvePrefix(commandConfig = {}) {
|
|
282
|
+
if (commandConfig.shell === true) return '';
|
|
283
|
+
if (Object.prototype.hasOwnProperty.call(commandConfig, 'prefix')) {
|
|
284
|
+
return this._normalizePrefix(commandConfig.prefix);
|
|
285
|
+
}
|
|
286
|
+
return this.commandPrefix;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Format a command for display, honoring its resolved prefix.
|
|
290
|
+
_displayCommand(command, commandConfig = {}) {
|
|
291
|
+
const prefix = this._resolvePrefix(commandConfig);
|
|
292
|
+
return prefix ? `${prefix} ${command}` : command;
|
|
293
|
+
}
|
|
294
|
+
|
|
266
295
|
async executeCommand(commandConfig, visited = new Set(), phaseName = null) {
|
|
267
296
|
const {
|
|
268
297
|
command,
|
|
@@ -281,6 +310,8 @@ export class Orchestrator {
|
|
|
281
310
|
} = commandConfig;
|
|
282
311
|
|
|
283
312
|
const startTime = Date.now();
|
|
313
|
+
// Effective invocation prefix for this command ('' => run verbatim as a shell command).
|
|
314
|
+
const prefix = this._resolvePrefix(commandConfig);
|
|
284
315
|
|
|
285
316
|
// Record the destination log file for this command (honors per-command override).
|
|
286
317
|
// Done early so even disabled/skipped commands report where output would land.
|
|
@@ -303,7 +334,7 @@ export class Orchestrator {
|
|
|
303
334
|
|
|
304
335
|
// Skip execution if the command is disabled
|
|
305
336
|
if (status === 'disabled') {
|
|
306
|
-
this.logger.warn(`Skipping:
|
|
337
|
+
this.logger.warn(`Skipping: ${this._displayCommand(command, commandConfig)} (status: disabled)`);
|
|
307
338
|
this.skippedCommands.push(command);
|
|
308
339
|
this.skipReasons.set(command, 'disabled');
|
|
309
340
|
setTiming(Date.now() - startTime);
|
|
@@ -336,6 +367,7 @@ export class Orchestrator {
|
|
|
336
367
|
startedByScript: false,
|
|
337
368
|
process_tracking,
|
|
338
369
|
kill_command,
|
|
370
|
+
prefix,
|
|
339
371
|
});
|
|
340
372
|
setTiming(Date.now() - startTime);
|
|
341
373
|
visited.delete(command);
|
|
@@ -411,6 +443,7 @@ export class Orchestrator {
|
|
|
411
443
|
env,
|
|
412
444
|
reportTime: this.metrics.includes('time'),
|
|
413
445
|
reportMemory: this.metrics.includes('memory'),
|
|
446
|
+
prefix,
|
|
414
447
|
});
|
|
415
448
|
lastRunResult = runResult;
|
|
416
449
|
const { success, output } = runResult;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Orchestrator } from './orchestrator.js';
|
|
2
|
+
|
|
3
|
+
const baseConfig = (extra = {}) => ({
|
|
4
|
+
phases: [{ name: 'p', parallel: [{ command: 'build' }] }],
|
|
5
|
+
...extra,
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
describe('Orchestrator command prefix resolution', () => {
|
|
9
|
+
test('defaults to "npm run" when no command_prefix is configured', () => {
|
|
10
|
+
const orch = new Orchestrator(baseConfig());
|
|
11
|
+
expect(orch.commandPrefix).toBe('npm run');
|
|
12
|
+
expect(orch._resolvePrefix({ command: 'build' })).toBe('npm run');
|
|
13
|
+
expect(orch._displayCommand('build', { command: 'build' })).toBe('npm run build');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('global command_prefix can be disabled with empty string', () => {
|
|
17
|
+
const orch = new Orchestrator(baseConfig({ command_prefix: '' }));
|
|
18
|
+
expect(orch.commandPrefix).toBe('');
|
|
19
|
+
expect(orch._resolvePrefix({ command: 'ls -la' })).toBe('');
|
|
20
|
+
expect(orch._displayCommand('ls -la', { command: 'ls -la' })).toBe('ls -la');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('global command_prefix can be disabled with false or null', () => {
|
|
24
|
+
expect(new Orchestrator(baseConfig({ command_prefix: false })).commandPrefix).toBe('');
|
|
25
|
+
expect(new Orchestrator(baseConfig({ command_prefix: null })).commandPrefix).toBe('');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('global command_prefix can be set to a custom runner', () => {
|
|
29
|
+
const orch = new Orchestrator(baseConfig({ command_prefix: 'pnpm run' }));
|
|
30
|
+
expect(orch._resolvePrefix({ command: 'build' })).toBe('pnpm run');
|
|
31
|
+
expect(orch._displayCommand('build', { command: 'build' })).toBe('pnpm run build');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('per-command shell:true runs verbatim as a bash command (overrides global)', () => {
|
|
35
|
+
const orch = new Orchestrator(baseConfig({ command_prefix: 'npm run' }));
|
|
36
|
+
const cmd = { command: 'echo hello && ls', shell: true };
|
|
37
|
+
expect(orch._resolvePrefix(cmd)).toBe('');
|
|
38
|
+
expect(orch._displayCommand('echo hello && ls', cmd)).toBe('echo hello && ls');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('per-command prefix string overrides the global prefix', () => {
|
|
42
|
+
const orch = new Orchestrator(baseConfig({ command_prefix: 'npm run' }));
|
|
43
|
+
const cmd = { command: 'build', prefix: 'yarn' };
|
|
44
|
+
expect(orch._resolvePrefix(cmd)).toBe('yarn');
|
|
45
|
+
expect(orch._displayCommand('build', cmd)).toBe('yarn build');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('per-command empty prefix runs verbatim even when global prefix is set', () => {
|
|
49
|
+
const orch = new Orchestrator(baseConfig({ command_prefix: 'npm run' }));
|
|
50
|
+
const cmd = { command: 'make build', prefix: '' };
|
|
51
|
+
expect(orch._resolvePrefix(cmd)).toBe('');
|
|
52
|
+
expect(orch._displayCommand('make build', cmd)).toBe('make build');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('shell:true takes precedence over an explicit prefix', () => {
|
|
56
|
+
const orch = new Orchestrator(baseConfig());
|
|
57
|
+
const cmd = { command: 'echo hi', shell: true, prefix: 'yarn' };
|
|
58
|
+
expect(orch._resolvePrefix(cmd)).toBe('');
|
|
59
|
+
});
|
|
60
|
+
});
|
package/lib/process-manager.js
CHANGED
|
@@ -46,6 +46,7 @@ export class ProcessManager {
|
|
|
46
46
|
startedByScript,
|
|
47
47
|
process_tracking,
|
|
48
48
|
kill_command,
|
|
49
|
+
prefix = 'npm run',
|
|
49
50
|
}) {
|
|
50
51
|
this.logger.verbose(`Adding background process: ${command} (${url})`);
|
|
51
52
|
this.backgroundProcessesDetails.push({
|
|
@@ -54,6 +55,7 @@ export class ProcessManager {
|
|
|
54
55
|
startedByScript,
|
|
55
56
|
process_tracking,
|
|
56
57
|
kill_command,
|
|
58
|
+
prefix,
|
|
57
59
|
});
|
|
58
60
|
}
|
|
59
61
|
|
|
@@ -93,7 +95,13 @@ export class ProcessManager {
|
|
|
93
95
|
env = null,
|
|
94
96
|
reportTime = false,
|
|
95
97
|
reportMemory = false,
|
|
98
|
+
prefix = 'npm run',
|
|
96
99
|
}) {
|
|
100
|
+
// Resolve how the command is invoked. A non-empty prefix (e.g. 'npm run') is
|
|
101
|
+
// prepended to the command name; an empty/false prefix runs the command verbatim
|
|
102
|
+
// as a regular shell command. `displayCmd` is what we surface in logs.
|
|
103
|
+
const commandPrefix = prefix ? String(prefix).trim() : '';
|
|
104
|
+
const displayCmd = commandPrefix ? `${commandPrefix} ${cmd}` : cmd;
|
|
97
105
|
const baseDir = this.logFolder
|
|
98
106
|
? path.resolve(this.logFolder)
|
|
99
107
|
: process.cwd();
|
|
@@ -131,12 +139,12 @@ export class ProcessManager {
|
|
|
131
139
|
const startTime = Date.now();
|
|
132
140
|
let timeOutputPath = null;
|
|
133
141
|
// Build command with environment variables if provided
|
|
134
|
-
let fullCommand =
|
|
142
|
+
let fullCommand = displayCmd;
|
|
135
143
|
if (env && Object.keys(env).length > 0) {
|
|
136
144
|
const envStr = Object.entries(env)
|
|
137
145
|
.map(([key, value]) => `${key}=${value}`)
|
|
138
146
|
.join(' ');
|
|
139
|
-
fullCommand = `${envStr}
|
|
147
|
+
fullCommand = `${envStr} ${displayCmd}`;
|
|
140
148
|
}
|
|
141
149
|
const useTimeWrapper =
|
|
142
150
|
reportMemory && !background && (process.platform === 'linux' || process.platform === 'darwin');
|
|
@@ -286,6 +294,7 @@ export class ProcessManager {
|
|
|
286
294
|
url: healthCheck?.url,
|
|
287
295
|
startedByScript: true,
|
|
288
296
|
kill_command,
|
|
297
|
+
prefix: commandPrefix,
|
|
289
298
|
});
|
|
290
299
|
|
|
291
300
|
this.logger.verbose(`Unreferencing process ${processGroupId}`);
|
|
@@ -293,7 +302,7 @@ export class ProcessManager {
|
|
|
293
302
|
|
|
294
303
|
this.logger.stopTask(cmd);
|
|
295
304
|
this.logger.verbose(
|
|
296
|
-
`Background process started:
|
|
305
|
+
`Background process started: ${displayCmd} (PGID: ${processGroupId})`,
|
|
297
306
|
);
|
|
298
307
|
return {
|
|
299
308
|
success: true,
|
|
@@ -304,7 +313,7 @@ export class ProcessManager {
|
|
|
304
313
|
} catch (error) {
|
|
305
314
|
if (attempt === maxAttempts) {
|
|
306
315
|
this.logger.error(
|
|
307
|
-
`Failed to start background process:
|
|
316
|
+
`Failed to start background process: ${displayCmd}`,
|
|
308
317
|
);
|
|
309
318
|
this.logger.verbose(
|
|
310
319
|
`Final verification attempt failed: ${error.message}`,
|
|
@@ -382,7 +391,7 @@ export class ProcessManager {
|
|
|
382
391
|
|
|
383
392
|
if (code !== 0) {
|
|
384
393
|
this.logger.error(
|
|
385
|
-
`Failed:
|
|
394
|
+
`Failed: ${displayCmd} ❌${durationStr} (exit code: ${code})`,
|
|
386
395
|
);
|
|
387
396
|
this.logger.verbose(`Process output: ${output}`);
|
|
388
397
|
resolve({
|
|
@@ -392,7 +401,7 @@ export class ProcessManager {
|
|
|
392
401
|
memoryKb,
|
|
393
402
|
});
|
|
394
403
|
} else {
|
|
395
|
-
this.logger.success(`Completed:
|
|
404
|
+
this.logger.success(`Completed: ${displayCmd} ✅${durationStr}`);
|
|
396
405
|
resolve({
|
|
397
406
|
success: true,
|
|
398
407
|
output,
|
|
@@ -507,13 +516,14 @@ export class ProcessManager {
|
|
|
507
516
|
);
|
|
508
517
|
|
|
509
518
|
const killPromises = commandProcesses.map(
|
|
510
|
-
async ({ command, pgid, url, startedByScript, kill_command }) => {
|
|
519
|
+
async ({ command, pgid, url, startedByScript, kill_command, prefix }) => {
|
|
511
520
|
await this.cleanupProcess({
|
|
512
521
|
command,
|
|
513
522
|
pgid,
|
|
514
523
|
url,
|
|
515
524
|
startedByScript,
|
|
516
525
|
kill_command,
|
|
526
|
+
prefix,
|
|
517
527
|
});
|
|
518
528
|
},
|
|
519
529
|
);
|
|
@@ -529,7 +539,7 @@ export class ProcessManager {
|
|
|
529
539
|
);
|
|
530
540
|
}
|
|
531
541
|
|
|
532
|
-
async cleanupProcess({ command, pgid, url, startedByScript, kill_command }) {
|
|
542
|
+
async cleanupProcess({ command, pgid, url, startedByScript, kill_command, prefix = 'npm run' }) {
|
|
533
543
|
if (!startedByScript) {
|
|
534
544
|
this.logger.verbose(
|
|
535
545
|
`- Skipping cleanup for ${command} (${url}) as it was not started by this script`,
|
|
@@ -544,13 +554,15 @@ export class ProcessManager {
|
|
|
544
554
|
// Try custom kill command first if specified
|
|
545
555
|
if (kill_command) {
|
|
546
556
|
try {
|
|
557
|
+
const killDisplay = prefix ? `${String(prefix).trim()} ${kill_command}` : kill_command;
|
|
547
558
|
this.logger.verbose(
|
|
548
|
-
`- Using custom kill command:
|
|
559
|
+
`- Using custom kill command: ${killDisplay}`,
|
|
549
560
|
);
|
|
550
561
|
const result = await this.runCommand({
|
|
551
562
|
cmd: kill_command,
|
|
552
563
|
logFile: null,
|
|
553
564
|
background: false,
|
|
565
|
+
prefix,
|
|
554
566
|
});
|
|
555
567
|
if (result.success) {
|
|
556
568
|
this.logger.verbose(
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import os from 'os';
|
|
2
4
|
import { ProcessManager } from './process-manager.js';
|
|
3
5
|
|
|
4
6
|
describe('ProcessManager.getLogPath', () => {
|
|
@@ -20,3 +22,59 @@ describe('ProcessManager.getLogPath', () => {
|
|
|
20
22
|
expect(result).toBe(path.resolve(override));
|
|
21
23
|
});
|
|
22
24
|
});
|
|
25
|
+
|
|
26
|
+
describe('ProcessManager.runCommand prefix handling', () => {
|
|
27
|
+
let tmpDir;
|
|
28
|
+
let prevCwd;
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'so-prefix-'));
|
|
32
|
+
prevCwd = process.cwd();
|
|
33
|
+
process.chdir(tmpDir);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
process.chdir(prevCwd);
|
|
38
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('runs a regular bash command verbatim when prefix is disabled', async () => {
|
|
42
|
+
const pm = new ProcessManager();
|
|
43
|
+
pm.setLogFolder(tmpDir);
|
|
44
|
+
const marker = 'orchestrator-raw-bash-ok';
|
|
45
|
+
const result = await pm.runCommand({
|
|
46
|
+
cmd: `echo ${marker}`,
|
|
47
|
+
background: false,
|
|
48
|
+
prefix: '',
|
|
49
|
+
});
|
|
50
|
+
expect(result.success).toBe(true);
|
|
51
|
+
const logPath = pm.getLogPath(`echo ${marker}`);
|
|
52
|
+
expect(fs.readFileSync(logPath, 'utf8')).toContain(marker);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('honors a custom prefix by invoking it (failure surfaces a non-zero exit)', async () => {
|
|
56
|
+
const pm = new ProcessManager();
|
|
57
|
+
pm.setLogFolder(tmpDir);
|
|
58
|
+
// With prefix 'npm run' and no package.json script, the command must fail —
|
|
59
|
+
// proving the prefix is actually prepended rather than the command run raw.
|
|
60
|
+
const result = await pm.runCommand({
|
|
61
|
+
cmd: 'definitely-not-a-script',
|
|
62
|
+
background: false,
|
|
63
|
+
prefix: 'npm run',
|
|
64
|
+
});
|
|
65
|
+
expect(result.success).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('supports multi-token shell commands (pipes, &&) when run raw', async () => {
|
|
69
|
+
const pm = new ProcessManager();
|
|
70
|
+
pm.setLogFolder(tmpDir);
|
|
71
|
+
const result = await pm.runCommand({
|
|
72
|
+
cmd: 'printf "a\\nb\\nc\\n" | grep b',
|
|
73
|
+
background: false,
|
|
74
|
+
prefix: '',
|
|
75
|
+
});
|
|
76
|
+
expect(result.success).toBe(true);
|
|
77
|
+
const logPath = pm.getLogPath('printf');
|
|
78
|
+
expect(fs.readFileSync(logPath, 'utf8').trim()).toBe('b');
|
|
79
|
+
});
|
|
80
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scripts-orchestrator",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.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,8 @@
|
|
|
1
1
|
export default {
|
|
2
|
+
// Optional: prefix prepended to every command. Defaults to 'npm run'.
|
|
3
|
+
// Set to '' (or false/null) to run commands verbatim as regular shell commands,
|
|
4
|
+
// or to another runner like 'pnpm run'. Per-command `shell: true` / `prefix` override this.
|
|
5
|
+
// command_prefix: 'npm run',
|
|
2
6
|
// Optional: metrics to report (time, memory). CLI --metrics overrides.
|
|
3
7
|
// metrics: ['time'],
|
|
4
8
|
// Optional: path for JSON results, or '-' for stdout. CLI --json-results overrides.
|