concurrently 9.2.1 → 10.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.
Files changed (149) hide show
  1. package/README.md +25 -14
  2. package/dist/bin/{concurrently.js → index.js} +23 -62
  3. package/dist/bin/index.spec.d.ts +1 -0
  4. package/dist/bin/index.spec.js +368 -0
  5. package/dist/bin/normalize-cli-command.d.ts +1 -0
  6. package/dist/bin/normalize-cli-command.js +15 -0
  7. package/dist/bin/normalize-cli-command.spec.d.ts +1 -0
  8. package/dist/bin/normalize-cli-command.spec.js +36 -0
  9. package/dist/bin/read-package-json.d.ts +4 -0
  10. package/dist/bin/read-package-json.js +17 -0
  11. package/dist/lib/__fixtures__/create-mock-instance.d.ts +2 -0
  12. package/dist/lib/__fixtures__/create-mock-instance.js +5 -0
  13. package/dist/lib/__fixtures__/fake-command.d.ts +6 -0
  14. package/dist/lib/__fixtures__/fake-command.js +37 -0
  15. package/dist/lib/assert.d.ts +10 -0
  16. package/dist/lib/assert.js +24 -0
  17. package/dist/lib/assert.spec.d.ts +1 -0
  18. package/dist/lib/assert.spec.js +41 -0
  19. package/dist/{src → lib}/command-parser/expand-arguments.d.ts +2 -2
  20. package/dist/{src → lib}/command-parser/expand-arguments.js +7 -12
  21. package/dist/lib/command-parser/expand-arguments.spec.d.ts +1 -0
  22. package/dist/lib/command-parser/expand-arguments.spec.js +57 -0
  23. package/dist/{src → lib}/command-parser/expand-shortcut.d.ts +2 -2
  24. package/dist/{src → lib}/command-parser/expand-shortcut.js +1 -5
  25. package/dist/lib/command-parser/expand-shortcut.spec.d.ts +1 -0
  26. package/dist/lib/command-parser/expand-shortcut.spec.js +36 -0
  27. package/dist/{src → lib}/command-parser/expand-wildcard.d.ts +2 -2
  28. package/dist/{src → lib}/command-parser/expand-wildcard.js +21 -28
  29. package/dist/lib/command-parser/expand-wildcard.spec.d.ts +1 -0
  30. package/dist/lib/command-parser/expand-wildcard.spec.js +288 -0
  31. package/dist/{src → lib}/command.d.ts +5 -4
  32. package/dist/{src → lib}/command.js +3 -39
  33. package/dist/lib/command.spec.d.ts +1 -0
  34. package/dist/lib/command.spec.js +369 -0
  35. package/dist/{src → lib}/completion-listener.d.ts +2 -2
  36. package/dist/{src → lib}/completion-listener.js +9 -46
  37. package/dist/lib/completion-listener.spec.d.ts +1 -0
  38. package/dist/lib/completion-listener.spec.js +229 -0
  39. package/dist/{src → lib}/concurrently.d.ts +10 -10
  40. package/dist/{src → lib}/concurrently.js +29 -40
  41. package/dist/lib/concurrently.spec.d.ts +1 -0
  42. package/dist/lib/concurrently.spec.js +320 -0
  43. package/dist/{src → lib}/date-format.d.ts +2 -2
  44. package/dist/{src → lib}/date-format.js +97 -76
  45. package/dist/lib/date-format.spec.d.ts +1 -0
  46. package/dist/lib/date-format.spec.js +480 -0
  47. package/dist/{src → lib}/defaults.d.ts +2 -6
  48. package/dist/{src → lib}/defaults.js +16 -23
  49. package/dist/{src → lib}/flow-control/input-handler.d.ts +4 -4
  50. package/dist/{src → lib}/flow-control/input-handler.js +8 -44
  51. package/dist/lib/flow-control/input-handler.spec.d.ts +1 -0
  52. package/dist/lib/flow-control/input-handler.spec.js +116 -0
  53. package/dist/{src → lib}/flow-control/kill-on-signal.d.ts +3 -3
  54. package/dist/{src → lib}/flow-control/kill-on-signal.js +3 -7
  55. package/dist/lib/flow-control/kill-on-signal.spec.d.ts +1 -0
  56. package/dist/lib/flow-control/kill-on-signal.spec.js +63 -0
  57. package/dist/{src → lib}/flow-control/kill-others.d.ts +3 -3
  58. package/dist/{src → lib}/flow-control/kill-others.js +8 -12
  59. package/dist/lib/flow-control/kill-others.spec.d.ts +1 -0
  60. package/dist/lib/flow-control/kill-others.spec.js +98 -0
  61. package/dist/{src → lib}/flow-control/log-error.d.ts +3 -3
  62. package/dist/{src → lib}/flow-control/log-error.js +1 -5
  63. package/dist/lib/flow-control/log-error.spec.d.ts +1 -0
  64. package/dist/lib/flow-control/log-error.spec.js +33 -0
  65. package/dist/{src → lib}/flow-control/log-exit.d.ts +3 -3
  66. package/dist/{src → lib}/flow-control/log-exit.js +1 -5
  67. package/dist/lib/flow-control/log-exit.spec.d.ts +1 -0
  68. package/dist/lib/flow-control/log-exit.spec.js +24 -0
  69. package/dist/{src → lib}/flow-control/log-output.d.ts +3 -3
  70. package/dist/{src → lib}/flow-control/log-output.js +1 -5
  71. package/dist/lib/flow-control/log-output.spec.d.ts +1 -0
  72. package/dist/lib/flow-control/log-output.spec.js +33 -0
  73. package/dist/{src → lib}/flow-control/log-timings.d.ts +3 -3
  74. package/dist/{src → lib}/flow-control/log-timings.js +8 -45
  75. package/dist/lib/flow-control/log-timings.spec.d.ts +1 -0
  76. package/dist/lib/flow-control/log-timings.spec.js +89 -0
  77. package/dist/{src → lib}/flow-control/logger-padding.d.ts +3 -3
  78. package/dist/{src → lib}/flow-control/logger-padding.js +7 -7
  79. package/dist/lib/flow-control/logger-padding.spec.d.ts +1 -0
  80. package/dist/lib/flow-control/logger-padding.spec.js +60 -0
  81. package/dist/{src → lib}/flow-control/output-error-handler.d.ts +4 -4
  82. package/dist/{src → lib}/flow-control/output-error-handler.js +3 -7
  83. package/dist/lib/flow-control/output-error-handler.spec.d.ts +1 -0
  84. package/dist/lib/flow-control/output-error-handler.spec.js +41 -0
  85. package/dist/{src → lib}/flow-control/restart-process.d.ts +4 -4
  86. package/dist/lib/flow-control/restart-process.js +61 -0
  87. package/dist/lib/flow-control/restart-process.spec.d.ts +1 -0
  88. package/dist/lib/flow-control/restart-process.spec.js +127 -0
  89. package/dist/{src → lib}/flow-control/teardown.d.ts +4 -5
  90. package/dist/lib/flow-control/teardown.js +45 -0
  91. package/dist/lib/flow-control/teardown.spec.d.ts +1 -0
  92. package/dist/lib/flow-control/teardown.spec.js +93 -0
  93. package/dist/{src → lib}/index.d.ts +21 -19
  94. package/dist/lib/index.js +98 -0
  95. package/dist/{src → lib}/jsonc.js +3 -8
  96. package/dist/lib/jsonc.spec.d.ts +1 -0
  97. package/dist/lib/jsonc.spec.js +73 -0
  98. package/dist/{src → lib}/logger.d.ts +3 -2
  99. package/dist/{src → lib}/logger.js +109 -65
  100. package/dist/lib/logger.spec.d.ts +1 -0
  101. package/dist/lib/logger.spec.js +507 -0
  102. package/dist/{src → lib}/observables.d.ts +1 -1
  103. package/dist/{src → lib}/observables.js +3 -6
  104. package/dist/lib/observables.spec.d.ts +1 -0
  105. package/dist/lib/observables.spec.js +29 -0
  106. package/dist/{src → lib}/output-writer.d.ts +3 -3
  107. package/dist/{src → lib}/output-writer.js +4 -41
  108. package/dist/lib/output-writer.spec.d.ts +1 -0
  109. package/dist/lib/output-writer.spec.js +96 -0
  110. package/dist/lib/prefix-color-selector.d.ts +21 -0
  111. package/dist/{src → lib}/prefix-color-selector.js +14 -31
  112. package/dist/lib/prefix-color-selector.spec.d.ts +1 -0
  113. package/dist/lib/prefix-color-selector.spec.js +159 -0
  114. package/dist/{src → lib}/spawn.d.ts +14 -5
  115. package/dist/lib/spawn.js +105 -0
  116. package/dist/lib/spawn.spec.d.ts +1 -0
  117. package/dist/lib/spawn.spec.js +100 -0
  118. package/dist/lib/utils.d.ts +25 -0
  119. package/dist/lib/utils.js +52 -0
  120. package/dist/lib/utils.spec.d.ts +1 -0
  121. package/dist/lib/utils.spec.js +58 -0
  122. package/dist/tsconfig.tsbuildinfo +1 -1
  123. package/docs/README.md +6 -0
  124. package/docs/cli/prefixing.md +44 -4
  125. package/docs/shell-resolution.md +48 -0
  126. package/package.json +59 -75
  127. package/dist/bin/read-package.d.ts +0 -6
  128. package/dist/bin/read-package.js +0 -56
  129. package/dist/src/assert.d.ts +0 -5
  130. package/dist/src/assert.js +0 -15
  131. package/dist/src/command-parser/command-parser.d.ts +0 -19
  132. package/dist/src/command-parser/command-parser.js +0 -2
  133. package/dist/src/command-parser/strip-quotes.d.ts +0 -16
  134. package/dist/src/command-parser/strip-quotes.js +0 -17
  135. package/dist/src/flow-control/flow-controller.d.ts +0 -13
  136. package/dist/src/flow-control/flow-controller.js +0 -2
  137. package/dist/src/flow-control/restart-process.js +0 -98
  138. package/dist/src/flow-control/teardown.js +0 -82
  139. package/dist/src/index.js +0 -96
  140. package/dist/src/prefix-color-selector.d.ts +0 -11
  141. package/dist/src/spawn.js +0 -49
  142. package/dist/src/utils.d.ts +0 -10
  143. package/dist/src/utils.js +0 -16
  144. package/index.d.mts +0 -7
  145. package/index.d.ts +0 -11
  146. package/index.js +0 -14
  147. package/index.mjs +0 -9
  148. /package/dist/bin/{concurrently.d.ts → index.d.ts} +0 -0
  149. /package/dist/{src → lib}/jsonc.d.ts +0 -0
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![Latest Release](https://img.shields.io/github/v/release/open-cli-tools/concurrently?label=Release)](https://github.com/open-cli-tools/concurrently/releases)
4
4
  [![License](https://img.shields.io/github/license/open-cli-tools/concurrently?label=License)](https://github.com/open-cli-tools/concurrently/blob/main/LICENSE)
5
5
  [![Weekly Downloads on NPM](https://img.shields.io/npm/dw/concurrently?label=Downloads&logo=npm)](https://www.npmjs.com/package/concurrently)
6
- [![CI Status](https://img.shields.io/github/actions/workflow/status/open-cli-tools/concurrently/test.yml?label=CI&logo=github)](https://github.com/open-cli-tools/concurrently/actions/workflows/test.yml)
6
+ [![CI Status](https://img.shields.io/github/actions/workflow/status/open-cli-tools/concurrently/ci.yml?label=CI&logo=github)](https://github.com/open-cli-tools/concurrently/actions/workflows/ci.yml)
7
7
  [![Coverage Status](https://img.shields.io/coveralls/github/open-cli-tools/concurrently/main?label=Coverage&logo=coveralls)](https://coveralls.io/github/open-cli-tools/concurrently?branch=main)
8
8
 
9
9
  Run multiple commands concurrently.
@@ -67,11 +67,18 @@ concurrently 'command1 arg' 'command2 arg'
67
67
  Otherwise **concurrently** would try to run 4 separate commands:
68
68
  `command1`, `arg`, `command2`, `arg`.
69
69
 
70
- In package.json, escape quotes:
71
-
72
- ```bash
73
- "start": "concurrently 'command1 arg' 'command2 arg'"
74
- ```
70
+ > [!IMPORTANT]
71
+ > Windows only supports double quotes:
72
+ >
73
+ > ```bash
74
+ > concurrently "command1 arg" "command2 arg"
75
+ > ```
76
+ >
77
+ > Remember to escape the double quotes in your package.json when using Windows:
78
+ >
79
+ > ```json
80
+ > "start": "concurrently \"command1 arg\" \"command2 arg\""
81
+ > ```
75
82
 
76
83
  You can always check concurrently's flag list by running `concurrently --help`.
77
84
  For the version, run `concurrently --version`.
@@ -90,21 +97,25 @@ Check out documentation and other usage examples in the [`docs` directory](./doc
90
97
  - `options` (optional): an object containing any of the below:
91
98
  - `cwd`: the working directory to be used by all commands. Can be overridden per command.
92
99
  Default: `process.cwd()`.
100
+ - `shell`: shell executable used to run command strings. When unset, uses `npm_config_script_shell` if present (for example when run via `npm run`), otherwise `cmd.exe` on Windows or `/bin/sh` elsewhere. See [shell resolution](./docs/shell-resolution.md).
93
101
  - `defaultInputTarget`: the default input target when reading from `inputStream`.
94
102
  Default: `0`.
95
103
  - `handleInput`: when `true`, reads input from `process.stdin`.
96
- - `inputStream`: a [`Readable` stream](https://nodejs.org/dist/latest-v10.x/docs/api/stream.html#stream_readable_streams)
104
+ - `inputStream`: a [`Readable` stream](https://nodejs.org/docs/latest/api/stream.html#readable-streams)
97
105
  to read the input from. Should only be used in the rare instance you would like to stream anything other than `process.stdin`. Overrides `handleInput`.
98
106
  - `pauseInputStreamOnFinish`: by default, pauses the input stream (`process.stdin` when `handleInput` is enabled, or `inputStream` if provided) when all of the processes have finished. If you need to read from the input stream after `concurrently` has finished, set this to `false`. ([#252](https://github.com/kimmobrunfeldt/concurrently/issues/252)).
99
107
  - `killOthersOn`: once the first command exits with one of these statuses, kill other commands.
100
108
  Can be an array containing the strings `success` (status code zero) and/or `failure` (non-zero exit status).
101
109
  - `maxProcesses`: how many processes should run at once.
102
- - `outputStream`: a [`Writable` stream](https://nodejs.org/dist/latest-v10.x/docs/api/stream.html#stream_writable_streams)
110
+ - `outputStream`: a [`Writable` stream](https://nodejs.org/docs/latest/api/stream.html#writable-streams)
103
111
  to write logs to. Default: `process.stdout`.
104
112
  - `prefix`: the prefix type to use when logging processes output.
105
113
  Possible values: `index`, `pid`, `time`, `command`, `name`, `none`, or a template (eg `[{time} process: {pid}]`).
106
114
  Default: the name of the process, or its index if no name is set.
107
- - `prefixColors`: a list of colors or a string as supported by [chalk](https://www.npmjs.com/package/chalk) and additional style `auto` for an automatically picked color.
115
+ Templates can wrap any portion of the prefix with `{color}` and `{/color}` to restrict coloring to that region (eg `[{color}{name}{/color}]` colors only the name, leaving the brackets uncolored). If either marker is omitted the missing side is implicit, so a template with no markers is colored in full.
116
+ - `prefixColors`: a list of colors or a string as supported by [Chalk](https://www.npmjs.com/package/chalk) and additional style `auto` for an automatically picked color.
117
+ Supports all Chalk color functions: `#RRGGBB`, `bg#RRGGBB`, `hex()`, `bgHex()`, `rgb()`, `bgRgb()`, `ansi256()`, `bgAnsi256()`.
118
+ Functions and modifiers can be chained (e.g., `rgb(255,136,0).bold`, `black.bgHex(#00FF00).dim`).
108
119
  If concurrently would run more commands than there are colors, the last color is repeated, unless if the last color value is `auto` which means following colors are automatically picked to vary.
109
120
  Prefix colors specified per-command take precedence over this list.
110
121
  - `prefixLength`: how many characters to show when prefixing with `command`. Default: `10`
@@ -142,7 +153,7 @@ const { result } = concurrently(
142
153
  ],
143
154
  {
144
155
  prefix: 'name',
145
- killOthers: ['failure', 'success'],
156
+ killOthersOn: ['failure', 'success'],
146
157
  restartTries: 3,
147
158
  cwd: path.resolve(__dirname, 'scripts'),
148
159
  },
@@ -212,18 +223,18 @@ It contains the following properties:
212
223
 
213
224
  ## FAQ
214
225
 
215
- - Process exited with code _null_?
226
+ - Process exited with code `null`?
216
227
 
217
- From [Node child_process documentation](http://nodejs.org/api/child_process.html#child_process_event_exit), `exit` event:
228
+ From [Node child_process documentation](https://nodejs.org/docs/latest/api/child_process.html#event-exit), `exit` event:
218
229
 
219
230
  > This event is emitted after the child process ends. If the process
220
231
  > terminated normally, code is the final exit code of the process,
221
232
  > otherwise null. If the process terminated due to receipt of a signal,
222
233
  > signal is the string name of the signal, otherwise null.
223
234
 
224
- So _null_ means the process didn't terminate normally. This will make **concurrently**
235
+ So `null` means the process didn't terminate normally. This will make **concurrently**
225
236
  to return non-zero exit code too.
226
237
 
227
- - Does this work with the npm-replacements [yarn](https://github.com/yarnpkg/yarn), [pnpm](https://pnpm.js.org/), or [Bun](https://bun.sh/)?
238
+ - Does this work with the npm-replacements [yarn](https://yarnpkg.com/), [pnpm](https://pnpm.io/), or [Bun](https://bun.sh/)?
228
239
 
229
240
  Yes! In all examples above, you may replace "`npm`" with "`yarn`", "`pnpm`", or "`bun`".
@@ -1,53 +1,16 @@
1
1
  #!/usr/bin/env node
2
- "use strict";
3
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
- if (k2 === undefined) k2 = k;
5
- var desc = Object.getOwnPropertyDescriptor(m, k);
6
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
- desc = { enumerable: true, get: function() { return m[k]; } };
8
- }
9
- Object.defineProperty(o, k2, desc);
10
- }) : (function(o, m, k, k2) {
11
- if (k2 === undefined) k2 = k;
12
- o[k2] = m[k];
13
- }));
14
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
- Object.defineProperty(o, "default", { enumerable: true, value: v });
16
- }) : function(o, v) {
17
- o["default"] = v;
18
- });
19
- var __importStar = (this && this.__importStar) || (function () {
20
- var ownKeys = function(o) {
21
- ownKeys = Object.getOwnPropertyNames || function (o) {
22
- var ar = [];
23
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
- return ar;
25
- };
26
- return ownKeys(o);
27
- };
28
- return function (mod) {
29
- if (mod && mod.__esModule) return mod;
30
- var result = {};
31
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
- __setModuleDefault(result, mod);
33
- return result;
34
- };
35
- })();
36
- var __importDefault = (this && this.__importDefault) || function (mod) {
37
- return (mod && mod.__esModule) ? mod : { "default": mod };
38
- };
39
- Object.defineProperty(exports, "__esModule", { value: true });
40
- const yargs_1 = __importDefault(require("yargs"));
41
- const helpers_1 = require("yargs/helpers");
42
- const assert_1 = require("../src/assert");
43
- const defaults = __importStar(require("../src/defaults"));
44
- const index_1 = require("../src/index");
45
- const utils_1 = require("../src/utils");
46
- const read_package_1 = require("./read-package");
47
- const version = String((0, read_package_1.readPackage)().version);
2
+ import process from 'node:process';
3
+ import yargs from 'yargs';
4
+ import { hideBin } from 'yargs/helpers';
5
+ import * as defaults from '../lib/defaults.js';
6
+ import { concurrently } from '../lib/index.js';
7
+ import { castArray, splitOutsideParens } from '../lib/utils.js';
8
+ import { normalizeCliCommand } from './normalize-cli-command.js';
9
+ import { readPackageJson } from './read-package-json.js';
10
+ const version = String(readPackageJson().version);
48
11
  const epilogue = `For documentation and more examples, visit:\nhttps://github.com/open-cli-tools/concurrently/tree/v${version}/docs`;
49
12
  // Clean-up arguments (yargs expects only the arguments after the program name)
50
- const program = (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv))
13
+ const program = yargs(hideBin(process.argv))
51
14
  .parserConfiguration({
52
15
  // Avoids options that can be specified multiple times from requiring a `--` to pass commands
53
16
  'greedy-arrays': false,
@@ -77,11 +40,6 @@ const program = (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv))
77
40
  'Example names: "main,browser,server"',
78
41
  type: 'string',
79
42
  },
80
- 'name-separator': {
81
- describe: 'The character to split <names> on. Example usage:\n' +
82
- '-n "styles|scripts|server" --name-separator "|"',
83
- default: defaults.nameSeparator,
84
- },
85
43
  success: {
86
44
  alias: 's',
87
45
  describe: 'Which command(s) must exit with code 0 in order for concurrently exit with ' +
@@ -102,7 +60,7 @@ const program = (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv))
102
60
  type: 'boolean',
103
61
  },
104
62
  // This one is provided for free. Chalk reads this itself and removes colors.
105
- // https://www.npmjs.com/package/chalk#chalksupportscolor
63
+ // https://www.npmjs.com/package/chalk#supportscolor
106
64
  'no-color': {
107
65
  describe: 'Disables colors from logging',
108
66
  type: 'boolean',
@@ -123,6 +81,10 @@ const program = (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv))
123
81
  type: 'boolean',
124
82
  default: defaults.timings,
125
83
  },
84
+ shell: {
85
+ describe: 'Shell to run commands with. Defaults to cmd.exe on Windows and /bin/sh elsewhere.',
86
+ type: 'string',
87
+ },
126
88
  'passthrough-arguments': {
127
89
  alias: 'P',
128
90
  describe: 'Passthrough additional arguments to commands (accessible via placeholders) ' +
@@ -167,7 +129,7 @@ const program = (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv))
167
129
  },
168
130
  'prefix-colors': {
169
131
  alias: 'c',
170
- describe: 'Comma-separated list of chalk colors to use on prefixes. ' +
132
+ describe: 'Comma-separated list of Chalk colors to use on prefixes. ' +
171
133
  'If there are more commands than colors, the last color will be repeated.\n' +
172
134
  '- Available modifiers: reset, bold, dim, italic, underline, inverse, hidden, strikethrough\n' +
173
135
  '- Available colors: black, red, green, yellow, blue, magenta, cyan, white, gray, \n' +
@@ -221,24 +183,22 @@ const program = (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv))
221
183
  'Can be either the index or the name of the process.',
222
184
  },
223
185
  })
224
- .group(['m', 'n', 'name-separator', 's', 'r', 'no-color', 'hide', 'g', 'timings', 'P', 'teardown'], 'General')
186
+ .group(['m', 'n', 's', 'r', 'no-color', 'hide', 'g', 'timings', 'shell', 'P', 'teardown'], 'General')
225
187
  .group(['p', 'c', 'l', 't', 'pad-prefix'], 'Prefix styling')
226
188
  .group(['i', 'default-input-target'], 'Input handling')
227
189
  .group(['k', 'kill-others-on-fail', 'kill-signal', 'kill-timeout'], 'Killing other processes')
228
190
  .group(['restart-tries', 'restart-after'], 'Restarting')
229
191
  .epilogue(epilogue);
230
192
  const args = program.parseSync();
231
- (0, assert_1.assertDeprecated)(args.nameSeparator === defaults.nameSeparator, 'name-separator', 'Use commas as name separators instead.');
232
- // Get names of commands by the specified separator
233
- const names = (args.names || '').split(args.nameSeparator);
234
- const additionalArguments = (0, utils_1.castArray)(args['--'] ?? []).map(String);
193
+ const names = (args.names || '').split(',');
194
+ const additionalArguments = castArray(args['--'] ?? []).map(String);
235
195
  const commands = args.passthroughArguments ? args._ : args._.concat(additionalArguments);
236
196
  if (!commands.length) {
237
197
  program.showHelp();
238
198
  process.exit();
239
199
  }
240
- (0, index_1.concurrently)(commands.map((command, index) => ({
241
- command: String(command),
200
+ concurrently(commands.map((command, index) => ({
201
+ command: normalizeCliCommand(String(command)),
242
202
  name: names[index],
243
203
  })), {
244
204
  handleInput: args.handleInput,
@@ -255,7 +215,7 @@ if (!commands.length) {
255
215
  hide: args.hide.split(','),
256
216
  group: args.group,
257
217
  prefix: args.prefix,
258
- prefixColors: args.prefixColors.split(','),
218
+ prefixColors: splitOutsideParens(args.prefixColors, ','),
259
219
  prefixLength: args.prefixLength,
260
220
  padPrefix: args.padPrefix,
261
221
  restartDelay: args.restartAfter === 'exponential' ? 'exponential' : Number(args.restartAfter),
@@ -263,6 +223,7 @@ if (!commands.length) {
263
223
  successCondition: args.success,
264
224
  timestampFormat: args.timestampFormat,
265
225
  timings: args.timings,
226
+ shell: args.shell,
266
227
  teardown: args.teardown,
267
228
  additionalArguments: args.passthroughArguments ? additionalArguments : undefined,
268
229
  }).result.then(() => process.exit(0), () => process.exit(1));
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,368 @@
1
+ import { execSync, spawn } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import readline from 'node:readline';
5
+ import { subscribeSpyTo } from '@hirez_io/observer-spy';
6
+ import { sendCtrlC, spawnWithWrapper } from 'ctrlc-wrapper';
7
+ import Rx from 'rxjs';
8
+ import { map } from 'rxjs/operators';
9
+ import stringArgv from 'string-argv';
10
+ import { beforeAll, describe, expect, it } from 'vitest';
11
+ import { escapeRegExp } from '../lib/utils.js';
12
+ const isWindows = process.platform === 'win32';
13
+ const createKillMessage = (prefix, signal) => {
14
+ const map = {
15
+ SIGTERM: isWindows ? 1 : '(SIGTERM|143)',
16
+ // Could theoretically be anything (e.g. 0) if process has SIGINT handler
17
+ SIGINT: isWindows ? '(3221225786|0)' : '(SIGINT|130|0)',
18
+ };
19
+ return new RegExp(`${escapeRegExp(prefix)} exited with code ${map[signal] ?? signal}`);
20
+ };
21
+ const concurrentlyBin = path.join(__dirname, '..', 'dist', 'bin', 'index.js');
22
+ // Build once, then spawn the real CLI without a shell (see open-cli-tools/concurrently#346).
23
+ beforeAll(() => {
24
+ execSync('pnpm run build', { cwd: path.join(__dirname, '..'), stdio: 'pipe' });
25
+ }, 20_000);
26
+ /**
27
+ * Creates a child process running 'concurrently' with the given args.
28
+ * Returns observables for its combined stdout + stderr output, close events, pid, and stdin stream.
29
+ */
30
+ const run = (args, ctrlcWrapper) => {
31
+ const spawnFn = ctrlcWrapper ? spawnWithWrapper : spawn;
32
+ const child = spawnFn('node', [concurrentlyBin, ...stringArgv(args)], {
33
+ cwd: __dirname,
34
+ env: {
35
+ ...process.env,
36
+ // VS Code extension might allow colors, but this breaks assertions.
37
+ // Force colors to be disabled to avoid that.
38
+ FORCE_COLOR: '0',
39
+ },
40
+ });
41
+ const stdout = readline.createInterface({
42
+ input: child.stdout,
43
+ });
44
+ const stderr = readline.createInterface({
45
+ input: child.stderr,
46
+ });
47
+ const log = new Rx.Observable((observer) => {
48
+ stdout.on('line', (line) => {
49
+ observer.next(line);
50
+ });
51
+ stderr.on('line', (line) => {
52
+ observer.next(line);
53
+ });
54
+ child.on('close', () => {
55
+ observer.complete();
56
+ });
57
+ });
58
+ const exit = Rx.firstValueFrom(Rx.fromEvent(child, 'exit').pipe(map((event) => {
59
+ const exit = event;
60
+ return {
61
+ /** The exit code if the child exited on its own. */
62
+ code: exit[0],
63
+ /** The signal by which the child process was terminated. */
64
+ signal: exit[1],
65
+ };
66
+ })));
67
+ const getLogLines = async () => {
68
+ const observerSpy = subscribeSpyTo(log);
69
+ await observerSpy.onComplete();
70
+ observerSpy.unsubscribe();
71
+ return observerSpy.getValues();
72
+ };
73
+ return {
74
+ process: child,
75
+ stdin: child.stdin,
76
+ pid: child.pid,
77
+ log,
78
+ getLogLines,
79
+ exit,
80
+ };
81
+ };
82
+ it('has help command', async () => {
83
+ const exit = await run('--help').exit;
84
+ expect(exit.code).toBe(0);
85
+ });
86
+ it('prints help when no arguments are passed', async () => {
87
+ const exit = await run('').exit;
88
+ expect(exit.code).toBe(0);
89
+ });
90
+ describe('has version command', () => {
91
+ const pkg = fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8');
92
+ const { version } = JSON.parse(pkg);
93
+ it.each(['--version', '-V', '-v'])('%s', async (arg) => {
94
+ const child = run(arg);
95
+ const log = await child.getLogLines();
96
+ expect(log).toContain(version);
97
+ const { code } = await child.exit;
98
+ expect(code).toBe(0);
99
+ });
100
+ });
101
+ describe('exiting conditions', () => {
102
+ it('is of success by default when running successful commands', async () => {
103
+ const exit = await run('"echo foo" "echo bar"').exit;
104
+ expect(exit.code).toBe(0);
105
+ });
106
+ it('strips outer CLI wrapper quotes before running a command', async () => {
107
+ const child = run('"echo foo"');
108
+ const lines = await child.getLogLines();
109
+ const exit = await child.exit;
110
+ expect(lines).toContainEqual(expect.stringContaining('foo'));
111
+ expect(exit.code).toBe(0);
112
+ });
113
+ it('is of failure by default when one of the command fails', async () => {
114
+ const exit = await run('"echo foo" "exit 1"').exit;
115
+ expect(exit.code).toBeGreaterThan(0);
116
+ });
117
+ it('is of success when --success=first and first command to exit succeeds', async () => {
118
+ const exit = await run('--success=first "echo foo" "node __fixtures__/sleep.js 0.5 && exit 1"').exit;
119
+ expect(exit.code).toBe(0);
120
+ });
121
+ it('is of failure when --success=first and first command to exit fails', async () => {
122
+ const exit = await run('--success=first "exit 1" "node __fixtures__/sleep.js 0.5 && echo foo"').exit;
123
+ expect(exit.code).toBeGreaterThan(0);
124
+ });
125
+ describe('is of success when --success=last and last command to exit succeeds', () => {
126
+ it.each(['--success=last', '-s last'])('%s', async (arg) => {
127
+ const exit = await run(`${arg} "exit 1" "node __fixtures__/sleep.js 0.5 && echo foo"`)
128
+ .exit;
129
+ expect(exit.code).toBe(0);
130
+ });
131
+ });
132
+ it('is of failure when --success=last and last command to exit fails', async () => {
133
+ const exit = await run('--success=last "echo foo" "node __fixtures__/sleep.js 0.5 && exit 1"').exit;
134
+ expect(exit.code).toBeGreaterThan(0);
135
+ });
136
+ it('is of success when a SIGINT is sent', async () => {
137
+ // Windows doesn't support sending signals like on POSIX platforms.
138
+ // However, in a console, processes can be interrupted with CTRL+C (like a SIGINT).
139
+ // This is what we simulate here with the help of a wrapper application.
140
+ const child = run('"node __fixtures__/read-echo.js"', isWindows);
141
+ // Wait for command to have started before sending SIGINT
142
+ child.log.subscribe((line) => {
143
+ if (/READING/.test(line)) {
144
+ if (isWindows) {
145
+ // Instruct the wrapper to send CTRL+C to its child
146
+ sendCtrlC(child.process);
147
+ }
148
+ else {
149
+ process.kill(Number(child.pid), 'SIGINT');
150
+ }
151
+ }
152
+ });
153
+ const lines = await child.getLogLines();
154
+ const exit = await child.exit;
155
+ expect(exit.code).toBe(0);
156
+ expect(lines).toContainEqual(expect.stringMatching(createKillMessage('[0] node __fixtures__/read-echo.js',
157
+ // TODO: Flappy value due to race condition, sometimes killed by concurrently (exit code 1),
158
+ // sometimes terminated on its own (exit code 0).
159
+ // Related issue: https://github.com/open-cli-tools/concurrently/issues/283
160
+ isWindows ? '(3221225786|0|1)' : 'SIGINT')));
161
+ });
162
+ });
163
+ describe('does not log any extra output', () => {
164
+ it.each(['--raw', '-r'])('%s', async (arg) => {
165
+ const lines = await run(`${arg} "echo foo" "echo bar"`).getLogLines();
166
+ expect(lines).toHaveLength(2);
167
+ expect(lines).toContainEqual(expect.stringContaining('foo'));
168
+ expect(lines).toContainEqual(expect.stringContaining('bar'));
169
+ });
170
+ });
171
+ describe('--hide', () => {
172
+ it('hides the output of a process by its index', async () => {
173
+ const lines = await run('--hide 1 "echo foo" "echo bar"').getLogLines();
174
+ expect(lines).toContainEqual(expect.stringContaining('foo'));
175
+ expect(lines).not.toContainEqual(expect.stringContaining('bar'));
176
+ });
177
+ it('hides the output of a process by its name', async () => {
178
+ const lines = await run('-n foo,bar --hide bar "echo foo" "echo bar"').getLogLines();
179
+ expect(lines).toContainEqual(expect.stringContaining('foo'));
180
+ expect(lines).not.toContainEqual(expect.stringContaining('bar'));
181
+ });
182
+ it('hides the output of a process by its index in raw mode', async () => {
183
+ const lines = await run('--hide 1 --raw "echo foo" "echo bar"').getLogLines();
184
+ expect(lines).toHaveLength(1);
185
+ expect(lines).toContainEqual(expect.stringContaining('foo'));
186
+ expect(lines).not.toContainEqual(expect.stringContaining('bar'));
187
+ });
188
+ it('hides the output of a process by its name in raw mode', async () => {
189
+ const lines = await run('-n foo,bar --hide bar --raw "echo foo" "echo bar"').getLogLines();
190
+ expect(lines).toHaveLength(1);
191
+ expect(lines).toContainEqual(expect.stringContaining('foo'));
192
+ expect(lines).not.toContainEqual(expect.stringContaining('bar'));
193
+ });
194
+ });
195
+ describe('--group', () => {
196
+ it('groups output per process', async () => {
197
+ const lines = await run('--group "echo foo && node __fixtures__/sleep.js 1 && echo bar" "echo baz"').getLogLines();
198
+ expect(lines.slice(0, 4)).toEqual([
199
+ expect.stringContaining('foo'),
200
+ expect.stringContaining('bar'),
201
+ expect.any(String),
202
+ expect.stringContaining('baz'),
203
+ ]);
204
+ });
205
+ });
206
+ describe('--names', () => {
207
+ describe('prefixes with names', () => {
208
+ it.each(['--names', '-n'])('%s', async (arg) => {
209
+ const lines = await run(`${arg} foo,bar "echo foo" "echo bar"`).getLogLines();
210
+ expect(lines).toContainEqual(expect.stringContaining('[foo] foo'));
211
+ expect(lines).toContainEqual(expect.stringContaining('[bar] bar'));
212
+ });
213
+ });
214
+ });
215
+ describe('specifies custom prefix', () => {
216
+ it.each(['--prefix', '-p'])('%s', async (arg) => {
217
+ const lines = await run(`${arg} command "echo foo" "echo bar"`).getLogLines();
218
+ expect(lines).toContainEqual(expect.stringContaining('[echo foo] foo'));
219
+ expect(lines).toContainEqual(expect.stringContaining('[echo bar] bar'));
220
+ });
221
+ });
222
+ describe('specifies custom prefix length', () => {
223
+ it.each(['--prefix command --prefix-length 5', '-p command -l 5'])('%s', async (arg) => {
224
+ const lines = await run(`${arg} "echo foo" "echo bar"`).getLogLines();
225
+ expect(lines).toContainEqual(expect.stringContaining('[ec..o] foo'));
226
+ expect(lines).toContainEqual(expect.stringContaining('[ec..r] bar'));
227
+ });
228
+ });
229
+ describe('--pad-prefix', () => {
230
+ it('pads prefixes with spaces', async () => {
231
+ const lines = await run('--pad-prefix -n foo,barbaz "echo foo" "echo bar"').getLogLines();
232
+ expect(lines).toContainEqual(expect.stringContaining('[foo ]'));
233
+ expect(lines).toContainEqual(expect.stringContaining('[barbaz]'));
234
+ });
235
+ });
236
+ describe('--restart-tries', () => {
237
+ it('changes how many times a command will restart', async () => {
238
+ const lines = await run('--restart-tries 1 "exit 1"').getLogLines();
239
+ expect(lines).toEqual([
240
+ expect.stringContaining('[0] exit 1 exited with code 1'),
241
+ expect.stringContaining('[0] exit 1 restarted'),
242
+ expect.stringContaining('[0] exit 1 exited with code 1'),
243
+ ]);
244
+ });
245
+ });
246
+ describe('--kill-others', () => {
247
+ describe('kills on success', () => {
248
+ it.each(['--kill-others', '-k'])('%s', async (arg) => {
249
+ const lines = await run(`${arg} "node __fixtures__/sleep.js 10" "exit 0"`).getLogLines();
250
+ expect(lines).toContainEqual(expect.stringContaining('[1] exit 0 exited with code 0'));
251
+ expect(lines).toContainEqual(expect.stringContaining('Sending SIGTERM to other processes'));
252
+ expect(lines).toContainEqual(expect.stringMatching(createKillMessage('[0] node __fixtures__/sleep.js 10', 'SIGTERM')));
253
+ });
254
+ });
255
+ it('kills on failure', async () => {
256
+ const lines = await run('--kill-others "node __fixtures__/sleep.js 10" "exit 1"').getLogLines();
257
+ expect(lines).toContainEqual(expect.stringContaining('[1] exit 1 exited with code 1'));
258
+ expect(lines).toContainEqual(expect.stringContaining('Sending SIGTERM to other processes'));
259
+ expect(lines).toContainEqual(expect.stringMatching(createKillMessage('[0] node __fixtures__/sleep.js 10', 'SIGTERM')));
260
+ });
261
+ });
262
+ describe('--kill-others-on-fail', () => {
263
+ it('does not kill on success', async () => {
264
+ const lines = await run('--kill-others-on-fail "node __fixtures__/sleep.js 0.5" "exit 0"').getLogLines();
265
+ expect(lines).toContainEqual(expect.stringContaining('[1] exit 0 exited with code 0'));
266
+ expect(lines).toContainEqual(expect.stringContaining('[0] node __fixtures__/sleep.js 0.5 exited with code 0'));
267
+ });
268
+ it('kills on failure', async () => {
269
+ const lines = await run('--kill-others-on-fail "node __fixtures__/sleep.js 10" "exit 1"').getLogLines();
270
+ expect(lines).toContainEqual(expect.stringContaining('[1] exit 1 exited with code 1'));
271
+ expect(lines).toContainEqual(expect.stringContaining('Sending SIGTERM to other processes'));
272
+ expect(lines).toContainEqual(expect.stringMatching(createKillMessage('[0] node __fixtures__/sleep.js 10', 'SIGTERM')));
273
+ });
274
+ });
275
+ describe('--handle-input', () => {
276
+ describe('forwards input to first process by default', () => {
277
+ it.each(['--handle-input', '-i'])('%s', async (arg) => {
278
+ const child = run(`${arg} "node __fixtures__/read-echo.js"`);
279
+ child.log.subscribe((line) => {
280
+ if (/READING/.test(line)) {
281
+ child.stdin.write('stop\n');
282
+ }
283
+ });
284
+ const lines = await child.getLogLines();
285
+ const exit = await child.exit;
286
+ expect(exit.code).toBe(0);
287
+ expect(lines).toContainEqual(expect.stringContaining('[0] stop'));
288
+ expect(lines).toContainEqual(expect.stringContaining('[0] node __fixtures__/read-echo.js exited with code 0'));
289
+ });
290
+ });
291
+ it('forwards input to process --default-input-target', async () => {
292
+ const child = run('-ki --default-input-target 1 "node __fixtures__/read-echo.js" "node __fixtures__/read-echo.js"');
293
+ child.log.subscribe((line) => {
294
+ if (/\[1\] READING/.test(line)) {
295
+ child.stdin.write('stop\n');
296
+ }
297
+ });
298
+ const lines = await child.getLogLines();
299
+ const exit = await child.exit;
300
+ expect(exit.code).toBeGreaterThan(0);
301
+ expect(lines).toContainEqual(expect.stringContaining('[1] stop'));
302
+ expect(lines).toContainEqual(expect.stringMatching(createKillMessage('[0] node __fixtures__/read-echo.js', 'SIGTERM')));
303
+ });
304
+ it('forwards input to specified process', async () => {
305
+ const child = run('-ki "node __fixtures__/read-echo.js" "node __fixtures__/read-echo.js"');
306
+ child.log.subscribe((line) => {
307
+ if (/\[1\] READING/.test(line)) {
308
+ child.stdin.write('1:stop\n');
309
+ }
310
+ });
311
+ const lines = await child.getLogLines();
312
+ const exit = await child.exit;
313
+ expect(exit.code).toBeGreaterThan(0);
314
+ expect(lines).toContainEqual(expect.stringContaining('[1] stop'));
315
+ expect(lines).toContainEqual(expect.stringMatching(createKillMessage('[0] node __fixtures__/read-echo.js', 'SIGTERM')));
316
+ });
317
+ });
318
+ describe('--teardown', () => {
319
+ it('runs teardown commands when input commands exit', async () => {
320
+ const lines = await run('--teardown "echo bye" "echo hey"').getLogLines();
321
+ expect(lines).toEqual([
322
+ expect.stringContaining('[0] hey'),
323
+ expect.stringContaining('[0] echo hey exited with code 0'),
324
+ expect.stringContaining('--> Running teardown command "echo bye"'),
325
+ expect.stringContaining('bye'),
326
+ expect.stringContaining('--> Teardown command "echo bye" exited with code 0'),
327
+ ]);
328
+ });
329
+ it('runs multiple teardown commands', async () => {
330
+ const lines = await run('--teardown "echo bye" --teardown "echo bye2" "echo hey"').getLogLines();
331
+ expect(lines).toContain('bye');
332
+ expect(lines).toContain('bye2');
333
+ });
334
+ });
335
+ describe('--timings', () => {
336
+ const defaultTimestampFormatRegex = /\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{3}/;
337
+ const tableTopBorderRegex = /^--> ┌[─┬]+┐$/;
338
+ const tableHeaderRowRegex = /^--> │ name +│ duration +│ exit code +│ killed +│ command +│$/;
339
+ const tableBottomBorderRegex = /^--> └[─┴]+┘$/;
340
+ const timingsTests = {
341
+ 'shows timings on success': ['node __fixtures__/sleep.js 0.5', 'exit 0'],
342
+ 'shows timings on failure': ['node __fixtures__/sleep.js 0.75', 'exit 1'],
343
+ };
344
+ it.each(Object.entries(timingsTests))('%s', async (_, commands) => {
345
+ const lines = await run(`--timings ${commands.map((command) => `"${command}"`).join(' ')}`).getLogLines();
346
+ // Expect output to contain process start / stop messages for each command
347
+ commands.forEach((command, index) => {
348
+ const escapedCommand = escapeRegExp(command);
349
+ expect(lines).toContainEqual(expect.stringMatching(new RegExp(`^\\[${index}] ${escapedCommand} started at ${defaultTimestampFormatRegex.source}$`)));
350
+ expect(lines).toContainEqual(expect.stringMatching(new RegExp(`^\\[${index}] ${escapedCommand} stopped at ${defaultTimestampFormatRegex.source} after (\\d|,)+ms$`)));
351
+ });
352
+ // Expect output to contain timings table
353
+ expect(lines).toContainEqual(expect.stringMatching(tableTopBorderRegex));
354
+ expect(lines).toContainEqual(expect.stringMatching(tableHeaderRowRegex));
355
+ expect(lines).toContainEqual(expect.stringMatching(tableBottomBorderRegex));
356
+ });
357
+ });
358
+ describe('--passthrough-arguments', () => {
359
+ it('argument placeholders are properly replaced when passthrough-arguments is enabled', async () => {
360
+ const lines = await run('--passthrough-arguments "echo {1}" -- echo').getLogLines();
361
+ expect(lines).toContainEqual(expect.stringContaining('[0] echo echo exited with code 0'));
362
+ });
363
+ it('argument placeholders are not replaced when passthrough-arguments is disabled', async () => {
364
+ const lines = await run('"echo {1}" -- echo').getLogLines();
365
+ expect(lines).toContainEqual(expect.stringContaining('[0] echo {1} exited with code 0'));
366
+ expect(lines).toContainEqual(expect.stringContaining('[1] echo exited with code 0'));
367
+ });
368
+ });
@@ -0,0 +1 @@
1
+ export declare function normalizeCliCommand(command: string): string;
@@ -0,0 +1,15 @@
1
+ export function normalizeCliCommand(command) {
2
+ if (command.length < 2) {
3
+ return command;
4
+ }
5
+ const quote = command[0];
6
+ const last = command.at(-1);
7
+ if ((quote !== '"' && quote !== "'") || last !== quote) {
8
+ return command;
9
+ }
10
+ const inner = command.slice(1, -1);
11
+ if (inner.includes(quote)) {
12
+ return command;
13
+ }
14
+ return inner;
15
+ }
@@ -0,0 +1 @@
1
+ export {};