• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

open-cli-tools / concurrently / 26038298601

18 May 2026 02:00PM UTC coverage: 97.977% (-1.0%) from 98.954%
26038298601

Pull #589

github

web-flow
Merge a222bd38d into 125a5679f
Pull Request #589: Allow shell overrides

463 of 475 branches covered (97.47%)

Branch coverage included in aggregate %.

24 of 26 new or added lines in 3 files covered. (92.31%)

7 existing lines in 1 file now uncovered.

796 of 810 relevant lines covered (98.27%)

548.41 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.89
/lib/spawn.ts
1
import assert from 'node:assert';
2
import { ChildProcess, IOType, spawn as baseSpawn, SpawnOptions } from 'node:child_process';
3
import path from 'node:path';
4
import nodeProcess from 'node:process';
5

6
import supportsColor, { ColorSupport } from 'supports-color';
7

8
import { SpawnCommand } from './command.js';
9
import { UnreachableError } from './utils.js';
10

11
/**
12
 * Spawns a command using `cmd.exe` on Windows, or `/bin/sh` elsewhere.
13
 */
14
// Implementation based off of https://github.com/mmalecki/spawn-command/blob/v0.0.2-1/lib/spawn-command.js
15
export function spawn(
16
    command: string,
17
    options: SpawnOptions,
18
    // For testing
19
    spawn: (command: string, args: string[], options: SpawnOptions) => ChildProcess = baseSpawn,
×
20
    process: Pick<NodeJS.Process, 'platform'> = nodeProcess,
×
21
): ChildProcess {
UNCOV
22
    let file = '/bin/sh';
×
UNCOV
23
    let args = ['-c', command];
×
UNCOV
24
    if (process.platform === 'win32') {
×
UNCOV
25
        file = 'cmd.exe';
×
UNCOV
26
        args = ['/s', '/c', `"${command}"`];
×
UNCOV
27
        options.windowsVerbatimArguments = true;
×
28
    }
UNCOV
29
    return spawn(file, args, options);
×
30
}
31

32
/**
33
 * Creates a spawn function that uses the given shell executable.
34
 *
35
 * The shell is resolved in the following priority order:
36
 * 1. explicit shell option
37
 * 2. `npm_config_script_shell` env variable
38
 * 3. platform default (`cmd.exe` on Windows, `/bin/sh` elsewhere)
39
 *
40
 * @see https://docs.npmjs.com/cli/v6/using-npm/config#script-shell
41
 */
42
export function createSpawn(
43
    shell?: string,
44
    // For testing
45
    spawn: (command: string, args: string[], options: SpawnOptions) => ChildProcess = baseSpawn,
81✔
46
    process: Pick<NodeJS.Process, 'platform' | 'env'> = nodeProcess,
81✔
47
): SpawnCommand {
48
    const resolved = resolveShell(shell, process);
81✔
49
    return (command, spawnOpts) => {
81✔
50
        const { file, args, shellOptions } = getShellSpawnArgs(resolved, command);
72✔
51
        return spawn(file, args, { ...spawnOpts, ...shellOptions });
72✔
52
    };
53
}
54

55
const NPM_SCRIPT_SHELL_ENV = 'npm_config_script_shell';
27✔
56

57
/**
58
 * Resolves which shell executable to use when spawning commands.
59
 * @see {@link createSpawn()}
60
 */
61
function resolveShell(
62
    shell?: string,
63
    process: Pick<NodeJS.Process, 'platform' | 'env'> = nodeProcess,
81✔
64
): string {
65
    if (shell) {
81✔
66
        return shell;
45✔
67
    }
68

69
    const npmScriptShell = process.env[NPM_SCRIPT_SHELL_ENV];
36✔
70
    if (npmScriptShell) {
36✔
71
        return npmScriptShell;
9✔
72
    }
73

74
    return process.platform === 'win32' ? 'cmd.exe' : '/bin/sh';
27✔
75
}
76

77
/**
78
 * Builds spawn file/args for the given shell and command string.
79
 */
80
function getShellSpawnArgs(
81
    shellPath: string,
82
    command: string,
83
): {
84
    file: string;
85
    args: string[];
86
    shellOptions?: Pick<SpawnOptions, 'windowsVerbatimArguments'>;
87
} {
88
    const kind = detectShellKind(shellPath);
72✔
89
    switch (kind) {
72!
90
        case 'cmd':
91
            return {
18✔
92
                file: shellPath,
93
                args: ['/s', '/c', `"${command}"`],
94
                shellOptions: { windowsVerbatimArguments: true },
95
            };
96
        case 'powershell':
97
            return {
9✔
98
                file: shellPath,
99
                args: ['-NoProfile', '-Command', command],
100
            };
101
        case 'posix':
102
            return {
45✔
103
                file: shellPath,
104
                args: ['-c', command],
105
            };
106
        default:
NEW
107
            throw new UnreachableError(kind);
×
108
    }
109
}
110

111
export type ShellKind = 'cmd' | 'posix' | 'powershell';
112

113
/**
114
 * Detects which argument style to use when spawning the given shell executable.
115
 */
116
function detectShellKind(shellPath: string): ShellKind {
117
    const normalized = shellPath.replace(/\\/g, '/');
72✔
118
    const base = path
72✔
119
        .basename(normalized)
120
        .toLowerCase()
121
        .replace(/\.exe$/i, '');
122

123
    if (base === 'cmd') {
72✔
124
        return 'cmd';
18✔
125
    }
126
    if (base === 'powershell' || base === 'pwsh') {
54✔
127
        return 'powershell';
9✔
128
    }
129
    return 'posix';
45✔
130
}
131

132
export const getSpawnOpts = ({
27✔
133
    colorSupport = supportsColor.stdout,
648✔
134
    cwd,
135
    process = nodeProcess,
648✔
136
    ipc,
137
    stdio = 'normal',
648✔
138
    env = {},
648✔
139
}: {
140
    /**
141
     * What the color support of the spawned processes should be.
142
     * If set to `false`, then no colors should be output.
143
     *
144
     * Defaults to whatever the terminal's stdout support is.
145
     */
146
    colorSupport?: Pick<ColorSupport, 'level'> | false;
147

148
    /**
149
     * The NodeJS process.
150
     */
151
    process?: Pick<NodeJS.Process, 'cwd' | 'platform' | 'env'>;
152

153
    /**
154
     * A custom working directory to spawn processes in.
155
     * Defaults to `process.cwd()`.
156
     */
157
    cwd?: string;
158

159
    /**
160
     * The file descriptor number at which the channel for inter-process communication
161
     * should be set up.
162
     */
163
    ipc?: number;
164

165
    /**
166
     * Which stdio mode to use. Raw implies inheriting the parent process' stdio.
167
     *
168
     * - `normal`: all of stdout, stderr and stdin are piped
169
     * - `hidden`: stdin is piped, stdout/stderr outputs are ignored
170
     * - `raw`: all of stdout, stderr and stdin are inherited from the main process
171
     *
172
     * Defaults to `normal`.
173
     */
174
    stdio?: 'normal' | 'hidden' | 'raw';
175

176
    /**
177
     * Map of custom environment variables to include in the spawn options.
178
     */
179
    env?: Record<string, unknown>;
180
}): SpawnOptions => {
181
    const stdioValues: (IOType | 'ipc')[] =
182
        stdio === 'normal'
648✔
183
            ? ['pipe', 'pipe', 'pipe']
184
            : stdio === 'raw'
171✔
185
              ? ['inherit', 'inherit', 'inherit']
186
              : ['pipe', 'ignore', 'ignore'];
187

188
    if (ipc != null) {
648✔
189
        // Avoid overriding the stdout/stderr/stdin
190
        assert.ok(ipc > 2, '[concurrently] the IPC channel number should be > 2');
18✔
191
        stdioValues[ipc] = 'ipc';
18✔
192
    }
193

194
    return {
639✔
195
        cwd: cwd || process.cwd(),
1,224✔
196
        stdio: stdioValues,
197
        ...(process.platform.startsWith('win') && { detached: false }),
885✔
198
        env: {
199
            ...(colorSupport ? { FORCE_COLOR: colorSupport.level.toString() } : {}),
639✔
200
            ...process.env,
201
            ...env,
202
        },
203
    };
204
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc