Repository: open-cli-tools/concurrently
Branch: main
Commit: 008b526de07d
Files: 111
Total size: 330.1 KB
Directory structure:
gitextract_injgqv22/
├── .editorconfig
├── .gitattributes
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug-report.yml
│ │ ├── config.yml
│ │ └── feature-request.yml
│ ├── actions/
│ │ └── setup/
│ │ └── action.yml
│ └── workflows/
│ ├── ci.yml
│ └── release.yml
├── .gitignore
├── .husky/
│ └── pre-commit
├── .node-version
├── .prettierignore
├── .prettierrc.json
├── .vscode/
│ ├── extensions.json
│ └── settings.json
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── bin/
│ ├── __fixtures__/
│ │ ├── read-echo.js
│ │ └── sleep.js
│ ├── index.spec.ts
│ ├── index.ts
│ └── read-package-json.ts
├── docs/
│ ├── README.md
│ └── cli/
│ ├── configuration.md
│ ├── input-handling.md
│ ├── output-control.md
│ ├── passthrough-arguments.md
│ ├── prefixing.md
│ ├── restarting.md
│ ├── shortcuts.md
│ ├── success.md
│ └── terminating.md
├── eslint.config.js
├── lib/
│ ├── __fixtures__/
│ │ ├── create-mock-instance.ts
│ │ └── fake-command.ts
│ ├── assert.spec.ts
│ ├── assert.ts
│ ├── command-parser/
│ │ ├── command-parser.d.ts
│ │ ├── expand-arguments.spec.ts
│ │ ├── expand-arguments.ts
│ │ ├── expand-shortcut.spec.ts
│ │ ├── expand-shortcut.ts
│ │ ├── expand-wildcard.spec.ts
│ │ ├── expand-wildcard.ts
│ │ ├── strip-quotes.spec.ts
│ │ └── strip-quotes.ts
│ ├── command.spec.ts
│ ├── command.ts
│ ├── completion-listener.spec.ts
│ ├── completion-listener.ts
│ ├── concurrently.spec.ts
│ ├── concurrently.ts
│ ├── date-format.spec.ts
│ ├── date-format.ts
│ ├── declarations/
│ │ └── intl.d.ts
│ ├── defaults.ts
│ ├── flow-control/
│ │ ├── flow-controller.d.ts
│ │ ├── input-handler.spec.ts
│ │ ├── input-handler.ts
│ │ ├── kill-on-signal.spec.ts
│ │ ├── kill-on-signal.ts
│ │ ├── kill-others.spec.ts
│ │ ├── kill-others.ts
│ │ ├── log-error.spec.ts
│ │ ├── log-error.ts
│ │ ├── log-exit.spec.ts
│ │ ├── log-exit.ts
│ │ ├── log-output.spec.ts
│ │ ├── log-output.ts
│ │ ├── log-timings.spec.ts
│ │ ├── log-timings.ts
│ │ ├── logger-padding.spec.ts
│ │ ├── logger-padding.ts
│ │ ├── output-error-handler.spec.ts
│ │ ├── output-error-handler.ts
│ │ ├── restart-process.spec.ts
│ │ ├── restart-process.ts
│ │ ├── teardown.spec.ts
│ │ └── teardown.ts
│ ├── index.ts
│ ├── jsonc.spec.ts
│ ├── jsonc.ts
│ ├── logger.spec.ts
│ ├── logger.ts
│ ├── observables.spec.ts
│ ├── observables.ts
│ ├── output-writer.spec.ts
│ ├── output-writer.ts
│ ├── prefix-color-selector.spec.ts
│ ├── prefix-color-selector.ts
│ ├── spawn.spec.ts
│ ├── spawn.ts
│ ├── utils.spec.ts
│ └── utils.ts
├── package.json
├── pnpm-workspace.yaml
├── tests/
│ ├── cjs-import/
│ │ ├── package.json
│ │ ├── smoke-test.ts
│ │ └── tsconfig.json
│ ├── cjs-require/
│ │ ├── package.json
│ │ ├── smoke-test.ts
│ │ └── tsconfig.json
│ ├── esm/
│ │ ├── package.json
│ │ ├── smoke-test.ts
│ │ └── tsconfig.json
│ ├── package.json
│ └── smoke-tests.spec.ts
├── tsconfig.json
└── vitest.config.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
# EditorConfig is awesome: https://editorconfig.org
root = true
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
indent_style = space
trim_trailing_whitespace = true
[*.{js,ts}]
indent_size = 4
max_line_length = 100
[*.md]
trim_trailing_whitespace = false
================================================
FILE: .gitattributes
================================================
* text=auto eol=lf
================================================
FILE: .github/FUNDING.yml
================================================
# Thank you! <3
github: [gustavohenke, paescuj]
================================================
FILE: .github/ISSUE_TEMPLATE/bug-report.yml
================================================
name: Bug Report
description: File a bug report
body:
- type: textarea
attributes:
label: Describe the bug
value: |
validations:
required: true
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
================================================
FILE: .github/ISSUE_TEMPLATE/feature-request.yml
================================================
name: Feature Request
description: Open a feature request
body:
- type: textarea
attributes:
label: Describe the feature request
value: |
validations:
required: true
================================================
FILE: .github/actions/setup/action.yml
================================================
name: Setup
description: Setup the environment for the project
inputs:
node-version:
description: Node.js version
required: false
node-registry:
description: Node.js package registry to set up for auth
required: false
runs:
using: composite
steps:
- name: Install Node.js
uses: actions/setup-node@v5
with:
node-version-file: ${{ !inputs.node-version && '.node-version' || '' }}
node-version: ${{ inputs.node-version }}
registry-url: ${{ inputs.node-registry }}
package-manager-cache: false
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Get pnpm store directory
id: pnpm-cache-dir
shell: bash
run: echo "dir=$(pnpm store path)" >> $GITHUB_OUTPUT
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache-dir.outputs.dir }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
shell: bash
run: pnpm install
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
push:
branches:
- main
pull_request:
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Setup
uses: ./.github/actions/setup
- name: Lint
run: pnpm run lint
- name: Format
run: pnpm run format
- name: Typecheck
run: pnpm run typecheck
test:
name: Test (Node.js ${{ matrix.node }}, ${{ matrix.os.name }})
runs-on: ${{ matrix.os.version }}
strategy:
fail-fast: false
matrix:
node:
- 20
- 22
- 24
os:
- name: Ubuntu
version: ubuntu-latest
- name: Windows
version: windows-latest
- name: macOS
version: macOS-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Setup
uses: ./.github/actions/setup
with:
node-version: ${{ matrix.node }}
- name: Test
run: pnpm exec vitest --coverage
- name: Submit coverage
uses: coverallsapp/github-action@master
continue-on-error: true
with:
github-token: ${{ secrets.github_token }}
flag-name: Node.js ${{ matrix.node }} on ${{ matrix.os.name }}
parallel: true
coverage:
name: Coverage
needs: test
runs-on: ubuntu-latest
continue-on-error: true
steps:
- name: Finish coverage
uses: coverallsapp/github-action@master
with:
github-token: ${{ secrets.github_token }}
parallel-finished: true
================================================
FILE: .github/workflows/release.yml
================================================
name: Release
on:
push:
tags:
- 'v*'
jobs:
gh-release:
name: Create GitHub Release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Create release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release create "$GITHUB_REF_NAME" --generate-notes
publish-npm:
name: Publish to NPM
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Setup
uses: ./.github/actions/setup
with:
node-registry: https://registry.npmjs.org
- name: Publish to NPM
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_CONFIG_PROVENANCE: true
run: pnpm publish --no-git-checks
================================================
FILE: .gitignore
================================================
# Outputs
dist
# Logs
*.log
# Coverage directory used by tools like istanbul
coverage
# Dependency directory
node_modules
# OS X
.DS_Store
================================================
FILE: .husky/pre-commit
================================================
./node_modules/.bin/lint-staged
================================================
FILE: .node-version
================================================
22
================================================
FILE: .prettierignore
================================================
dist
coverage
pnpm-lock.yaml
================================================
FILE: .prettierrc.json
================================================
{
"singleQuote": true
}
================================================
FILE: .vscode/extensions.json
================================================
{
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"editorconfig.editorconfig",
"vitest.explorer"
]
}
================================================
FILE: .vscode/settings.json
================================================
{
// For contributors with the Jest extension installed,
// it might incorrectly report errors in the suite
"jest.enable": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"[javascript, typescript]": {
"editor.formatOnSave": false
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.rulers": [100]
}
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing
Pull requests and contributions are warmly welcome.
Please follow existing code style and commit message conventions. Also remember to keep documentation
updated.
**Pull requests:** You don't need to bump version numbers or modify anything related to releasing. That stuff is fully automated, just write the functionality.
# Maintaining
## Code Format & Linting
Code format and lint checks are performed locally when committing to ensure the changes align with the configured rules of this repository. This happens with the help of the tools [simple-git-hooks](https://github.com/toplenboren/simple-git-hooks) and [lint-staged](https://github.com/okonet/lint-staged) which are automatically installed and configured on `pnpm install` (no further steps required).
In case of problems, a corresponding message is displayed in your terminal.
Please fix them and then run the commit command again.
## Test
Tests can be executed with the following command:
```bash
pnpm test
```
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2015 Kimmo Brunfeldt
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# concurrently
[](https://github.com/open-cli-tools/concurrently/releases)
[](https://github.com/open-cli-tools/concurrently/blob/main/LICENSE)
[](https://www.npmjs.com/package/concurrently)
[](https://github.com/open-cli-tools/concurrently/actions/workflows/test.yml)
[](https://coveralls.io/github/open-cli-tools/concurrently?branch=main)
Run multiple commands concurrently.
Like `npm run watch-js & npm run watch-less` but better.

**Table of Contents**
- [concurrently](#concurrently)
- [Why](#why)
- [Installation](#installation)
- [Usage](#usage)
- [API](#api)
- [`concurrently(commands[, options])`](#concurrentlycommands-options)
- [`Command`](#command)
- [`CloseEvent`](#closeevent)
- [FAQ](#faq)
## Why
I like [task automation with npm](https://web.archive.org/web/20220531064025/https://github.com/substack/blog/blob/master/npm_run.markdown)
but the usual way to run multiple commands concurrently is
`npm run watch-js & npm run watch-css`. That's fine but it's hard to keep
on track of different outputs. Also if one process fails, others still keep running
and you won't even notice the difference.
Another option would be to just run all commands in separate terminals. I got
tired of opening terminals and made **concurrently**.
**Features:**
- Cross platform (including Windows)
- Output is easy to follow with prefixes
- With `--kill-others` switch, all commands are killed if one dies
## Installation
**concurrently** can be installed in the global scope (if you'd like to have it available and use it on the whole system) or locally for a specific package (for example if you'd like to use it in the `scripts` section of your package):
| | npm | Yarn | pnpm | Bun |
| ----------- | ----------------------- | ------------------------------ | -------------------------- | ------------------------- |
| **Global** | `npm i -g concurrently` | `yarn global add concurrently` | `pnpm add -g concurrently` | `bun add -g concurrently` |
| **Local**\* | `npm i -D concurrently` | `yarn add -D concurrently` | `pnpm add -D concurrently` | `bun add -d concurrently` |
\* It's recommended to add **concurrently** to `devDependencies` as it's usually used for developing purposes. Please adjust the command if this doesn't apply in your case.
## Usage
> **Note**
> The `concurrently` command is also available under the shorthand alias `conc`.
The tool is written in Node.js, but you can use it to run **any** commands.
Remember to surround separate commands with quotes:
```bash
concurrently 'command1 arg' 'command2 arg'
```
Otherwise **concurrently** would try to run 4 separate commands:
`command1`, `arg`, `command2`, `arg`.
> [!IMPORTANT]
> Windows only supports double quotes:
>
> ```bash
> concurrently "command1 arg" "command2 arg"
> ```
>
> Remember to escape the double quotes in your package.json when using Windows:
>
> ```json
> "start": "concurrently \"command1 arg\" \"command2 arg\""
> ```
You can always check concurrently's flag list by running `concurrently --help`.
For the version, run `concurrently --version`.
Check out documentation and other usage examples in the [`docs` directory](./docs/README.md).
## API
**concurrently** can be used programmatically by using the API documented below:
### `concurrently(commands[, options])`
- `commands`: an array of either strings (containing the commands to run) or objects
with the shape `{ command, name, prefixColor, env, cwd, ipc }`.
- `options` (optional): an object containing any of the below:
- `cwd`: the working directory to be used by all commands. Can be overridden per command.
Default: `process.cwd()`.
- `defaultInputTarget`: the default input target when reading from `inputStream`.
Default: `0`.
- `handleInput`: when `true`, reads input from `process.stdin`.
- `inputStream`: a [`Readable` stream](https://nodejs.org/dist/latest-v10.x/docs/api/stream.html#stream_readable_streams)
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`.
- `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)).
- `killOthersOn`: once the first command exits with one of these statuses, kill other commands.
Can be an array containing the strings `success` (status code zero) and/or `failure` (non-zero exit status).
- `maxProcesses`: how many processes should run at once.
- `outputStream`: a [`Writable` stream](https://nodejs.org/dist/latest-v10.x/docs/api/stream.html#stream_writable_streams)
to write logs to. Default: `process.stdout`.
- `prefix`: the prefix type to use when logging processes output.
Possible values: `index`, `pid`, `time`, `command`, `name`, `none`, or a template (eg `[{time} process: {pid}]`).
Default: the name of the process, or its index if no name is set.
- `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.
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.
Prefix colors specified per-command take precedence over this list.
- `prefixLength`: how many characters to show when prefixing with `command`. Default: `10`
- `raw`: whether raw mode should be used, meaning strictly process output will
be logged, without any prefixes, coloring or extra stuff. Can be overridden per command.
- `successCondition`: the condition to consider the run was successful.
If `first`, only the first process to exit will make up the success of the run; if `last`, the last process that exits will determine whether the run succeeds.
Anything else means all processes should exit successfully.
- `restartTries`: how many attempts to restart a process that dies will be made. Default: `0`.
- `restartDelay`: how many milliseconds to wait between process restarts. Default: `0`.
- `timestampFormat`: a [Unicode format](https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table)
to use when prefixing with `time`. Default: `yyyy-MM-dd HH:mm:ss.SSS`
- `additionalArguments`: list of additional arguments passed that will get replaced in each command. If not defined, no argument replacing will happen.
> **Returns:** an object in the shape `{ result, commands }`.
>
> - `result`: a `Promise` that resolves if the run was successful (according to `successCondition` option),
> or rejects, containing an array of [`CloseEvent`](#CloseEvent), in the order that the commands terminated.
> - `commands`: an array of all spawned [`Command`s](#Command).
Example:
```js
const concurrently = require('concurrently');
const { result } = concurrently(
[
'npm:watch-*',
{ command: 'nodemon', name: 'server' },
{ command: 'deploy', name: 'deploy', env: { PUBLIC_KEY: '...' } },
{
command: 'watch',
name: 'watch',
cwd: path.resolve(__dirname, 'scripts/watchers'),
},
],
{
prefix: 'name',
killOthersOn: ['failure', 'success'],
restartTries: 3,
cwd: path.resolve(__dirname, 'scripts'),
},
);
result.then(success, failure);
```
### `Command`
An object that contains all information about a spawned command, and ways to interact with it.
It has the following properties:
- `index`: the index of the command among all commands spawned.
- `command`: the command line of the command.
- `name`: the name of the command; defaults to an empty string.
- `cwd`: the current working directory of the command.
- `env`: an object with all the environment variables that the command will be spawned with.
- `killed`: whether the command has been killed.
- `state`: the command's state. Can be one of
- `stopped`: if the command was never started
- `started`: if the command is currently running
- `errored`: if the command failed spawning
- `exited`: if the command is not running anymore, e.g. it received a close event
- `pid`: the command's process ID.
- `stdin`: a Writable stream to the command's `stdin`.
- `stdout`: an RxJS observable to the command's `stdout`.
- `stderr`: an RxJS observable to the command's `stderr`.
- `error`: an RxJS observable to the command's error events (e.g. when it fails to spawn).
- `timer`: an RxJS observable to the command's timing events (e.g. starting, stopping).
- `stateChange`: an RxJS observable for changes to the command's `state` property.
- `messages`: an object with the following properties:
- `incoming`: an RxJS observable for the IPC messages received from the underlying process.
- `outgoing`: an RxJS observable for the IPC messages sent to the underlying process.
Both observables emit [`MessageEvent`](#messageevent)s.
Note that if the command wasn't spawned with IPC support, these won't emit any values.
- `close`: an RxJS observable to the command's close events.
See [`CloseEvent`](#CloseEvent) for more information.
- `start()`: starts the command and sets up all of the above streams
- `send(message[, handle, options])`: sends a message to the underlying process via IPC channels,
returning a promise that resolves once the message has been sent.
See [Node.js docs](https://nodejs.org/docs/latest/api/child_process.html#subprocesssendmessage-sendhandle-options-callback).
- `kill([signal])`: kills the command, optionally specifying a signal (e.g. `SIGTERM`, `SIGKILL`, etc).
### `MessageEvent`
An object that represents a message that was received from/sent to the underlying command process.
It has the following properties:
- `message`: the message itself.
- `handle`: a [`net.Socket`](https://nodejs.org/docs/latest/api/net.html#class-netsocket),
[`net.Server`](https://nodejs.org/docs/latest/api/net.html#class-netserver) or
[`dgram.Socket`](https://nodejs.org/docs/latest/api/dgram.html#class-dgramsocket),
if one was sent, or `undefined`.
### `CloseEvent`
An object with information about a command's closing event.
It contains the following properties:
- `command`: a stripped down version of [`Command`](#command), including only `name`, `command`, `env` and `cwd` properties.
- `index`: the index of the command among all commands spawned.
- `killed`: whether the command exited because it was killed.
- `exitCode`: the exit code of the command's process, or the signal which it was killed with.
- `timings`: an object in the shape `{ startDate, endDate, durationSeconds }`.
## FAQ
- Process exited with code _null_?
From [Node child_process documentation](http://nodejs.org/api/child_process.html#child_process_event_exit), `exit` event:
> This event is emitted after the child process ends. If the process
> terminated normally, code is the final exit code of the process,
> otherwise null. If the process terminated due to receipt of a signal,
> signal is the string name of the signal, otherwise null.
So _null_ means the process didn't terminate normally. This will make **concurrently**
to return non-zero exit code too.
- Does this work with the npm-replacements [yarn](https://yarnpkg.com/), [pnpm](https://pnpm.io/), or [Bun](https://bun.sh/)?
Yes! In all examples above, you may replace "`npm`" with "`yarn`", "`pnpm`", or "`bun`".
================================================
FILE: bin/__fixtures__/read-echo.js
================================================
import process from 'node:process';
process.stdin.on('data', (chunk) => {
const line = chunk.toString().trim();
console.log(line);
if (line === 'stop') {
process.exit(0);
}
});
console.log('READING');
================================================
FILE: bin/__fixtures__/sleep.js
================================================
/*
* Platform independent implementation of 'sleep' used as a command in tests
*
* (Windows doesn't provide the 'sleep' command by default,
* see https://github.com/open-cli-tools/concurrently/issues/277)
*/
import process from 'node:process';
const seconds = +process.argv[2];
if (!seconds || Number.isNaN(seconds) || process.argv.length > 3) {
// Mimic behavior from native 'sleep' command
console.error('usage: sleep seconds');
process.exit(1);
}
await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
================================================
FILE: bin/index.spec.ts
================================================
import { spawn } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import readline from 'node:readline';
import { subscribeSpyTo } from '@hirez_io/observer-spy';
import { sendCtrlC, spawnWithWrapper } from 'ctrlc-wrapper';
import { build } from 'esbuild';
import Rx from 'rxjs';
import { map } from 'rxjs/operators';
import stringArgv from 'string-argv';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { escapeRegExp } from '../lib/utils.js';
const isWindows = process.platform === 'win32';
const createKillMessage = (prefix: string, signal: 'SIGTERM' | 'SIGINT' | string) => {
const map: Record = {
SIGTERM: isWindows ? 1 : '(SIGTERM|143)',
// Could theoretically be anything (e.g. 0) if process has SIGINT handler
SIGINT: isWindows ? '(3221225786|0)' : '(SIGINT|130|0)',
};
return new RegExp(`${escapeRegExp(prefix)} exited with code ${map[signal] ?? signal}`);
};
let tmpDir: string;
beforeAll(async () => {
// Build 'concurrently' and store it in a temporary directory
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'concurrently-'));
await build({
entryPoints: [path.join(__dirname, 'index.ts')],
platform: 'node',
bundle: true,
// it doesn't seem like esbuild is able to change a CJS module to ESM, so target CJS instead.
// https://github.com/evanw/esbuild/issues/1921
format: 'cjs',
outfile: path.join(tmpDir, 'concurrently.cjs'),
});
fs.copyFileSync(path.join(__dirname, '..', 'package.json'), path.join(tmpDir, 'package.json'));
}, 8000);
afterAll(() => {
// Remove the temporary directory where 'concurrently' was stored
if (tmpDir) {
fs.rmSync(tmpDir, { recursive: true });
}
});
/**
* Creates a child process running 'concurrently' with the given args.
* Returns observables for its combined stdout + stderr output, close events, pid, and stdin stream.
*/
const run = (args: string, ctrlcWrapper?: boolean) => {
const spawnFn = ctrlcWrapper ? spawnWithWrapper : spawn;
const child = spawnFn('node', [path.join(tmpDir, 'concurrently.cjs'), ...stringArgv(args)], {
cwd: __dirname,
env: {
...process.env,
},
});
const stdout = readline.createInterface({
input: child.stdout,
});
const stderr = readline.createInterface({
input: child.stderr,
});
const log = new Rx.Observable((observer) => {
stdout.on('line', (line) => {
observer.next(line);
});
stderr.on('line', (line) => {
observer.next(line);
});
child.on('close', () => {
observer.complete();
});
});
const exit = Rx.firstValueFrom(
Rx.fromEvent(child, 'exit').pipe(
map((event) => {
const exit = event as [number | null, NodeJS.Signals | null];
return {
/** The exit code if the child exited on its own. */
code: exit[0],
/** The signal by which the child process was terminated. */
signal: exit[1],
};
}),
),
);
const getLogLines = async (): Promise => {
const observerSpy = subscribeSpyTo(log);
await observerSpy.onComplete();
observerSpy.unsubscribe();
return observerSpy.getValues();
};
return {
process: child,
stdin: child.stdin,
pid: child.pid,
log,
getLogLines,
exit,
};
};
it('has help command', async () => {
const exit = await run('--help').exit;
expect(exit.code).toBe(0);
});
it('prints help when no arguments are passed', async () => {
const exit = await run('').exit;
expect(exit.code).toBe(0);
});
describe('has version command', () => {
const pkg = fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8');
const { version } = JSON.parse(pkg);
it.each(['--version', '-V', '-v'])('%s', async (arg) => {
const child = run(arg);
const log = await child.getLogLines();
expect(log).toContain(version);
const { code } = await child.exit;
expect(code).toBe(0);
});
});
describe('exiting conditions', () => {
it('is of success by default when running successful commands', async () => {
const exit = await run('"echo foo" "echo bar"').exit;
expect(exit.code).toBe(0);
});
it('is of failure by default when one of the command fails', async () => {
const exit = await run('"echo foo" "exit 1"').exit;
expect(exit.code).toBeGreaterThan(0);
});
it('is of success when --success=first and first command to exit succeeds', async () => {
const exit = await run(
'--success=first "echo foo" "node __fixtures__/sleep.js 0.5 && exit 1"',
).exit;
expect(exit.code).toBe(0);
});
it('is of failure when --success=first and first command to exit fails', async () => {
const exit = await run(
'--success=first "exit 1" "node __fixtures__/sleep.js 0.5 && echo foo"',
).exit;
expect(exit.code).toBeGreaterThan(0);
});
describe('is of success when --success=last and last command to exit succeeds', () => {
it.each(['--success=last', '-s last'])('%s', async (arg) => {
const exit = await run(`${arg} "exit 1" "node __fixtures__/sleep.js 0.5 && echo foo"`)
.exit;
expect(exit.code).toBe(0);
});
});
it('is of failure when --success=last and last command to exit fails', async () => {
const exit = await run(
'--success=last "echo foo" "node __fixtures__/sleep.js 0.5 && exit 1"',
).exit;
expect(exit.code).toBeGreaterThan(0);
});
it('is of success when a SIGINT is sent', async () => {
// Windows doesn't support sending signals like on POSIX platforms.
// However, in a console, processes can be interrupted with CTRL+C (like a SIGINT).
// This is what we simulate here with the help of a wrapper application.
const child = run('"node __fixtures__/read-echo.js"', isWindows);
// Wait for command to have started before sending SIGINT
child.log.subscribe((line) => {
if (/READING/.test(line)) {
if (isWindows) {
// Instruct the wrapper to send CTRL+C to its child
sendCtrlC(child.process);
} else {
process.kill(Number(child.pid), 'SIGINT');
}
}
});
const lines = await child.getLogLines();
const exit = await child.exit;
expect(exit.code).toBe(0);
expect(lines).toContainEqual(
expect.stringMatching(
createKillMessage(
'[0] node __fixtures__/read-echo.js',
// TODO: Flappy value due to race condition, sometimes killed by concurrently (exit code 1),
// sometimes terminated on its own (exit code 0).
// Related issue: https://github.com/open-cli-tools/concurrently/issues/283
isWindows ? '(3221225786|0|1)' : 'SIGINT',
),
),
);
});
});
describe('does not log any extra output', () => {
it.each(['--raw', '-r'])('%s', async (arg) => {
const lines = await run(`${arg} "echo foo" "echo bar"`).getLogLines();
expect(lines).toHaveLength(2);
expect(lines).toContainEqual(expect.stringContaining('foo'));
expect(lines).toContainEqual(expect.stringContaining('bar'));
});
});
describe('--hide', () => {
it('hides the output of a process by its index', async () => {
const lines = await run('--hide 1 "echo foo" "echo bar"').getLogLines();
expect(lines).toContainEqual(expect.stringContaining('foo'));
expect(lines).not.toContainEqual(expect.stringContaining('bar'));
});
it('hides the output of a process by its name', async () => {
const lines = await run('-n foo,bar --hide bar "echo foo" "echo bar"').getLogLines();
expect(lines).toContainEqual(expect.stringContaining('foo'));
expect(lines).not.toContainEqual(expect.stringContaining('bar'));
});
it('hides the output of a process by its index in raw mode', async () => {
const lines = await run('--hide 1 --raw "echo foo" "echo bar"').getLogLines();
expect(lines).toHaveLength(1);
expect(lines).toContainEqual(expect.stringContaining('foo'));
expect(lines).not.toContainEqual(expect.stringContaining('bar'));
});
it('hides the output of a process by its name in raw mode', async () => {
const lines = await run('-n foo,bar --hide bar --raw "echo foo" "echo bar"').getLogLines();
expect(lines).toHaveLength(1);
expect(lines).toContainEqual(expect.stringContaining('foo'));
expect(lines).not.toContainEqual(expect.stringContaining('bar'));
});
});
describe('--group', () => {
it('groups output per process', async () => {
const lines = await run(
'--group "echo foo && node __fixtures__/sleep.js 1 && echo bar" "echo baz"',
).getLogLines();
expect(lines.slice(0, 4)).toEqual([
expect.stringContaining('foo'),
expect.stringContaining('bar'),
expect.any(String),
expect.stringContaining('baz'),
]);
});
});
describe('--names', () => {
describe('prefixes with names', () => {
it.each(['--names', '-n'])('%s', async (arg) => {
const lines = await run(`${arg} foo,bar "echo foo" "echo bar"`).getLogLines();
expect(lines).toContainEqual(expect.stringContaining('[foo] foo'));
expect(lines).toContainEqual(expect.stringContaining('[bar] bar'));
});
});
it('is split using --name-separator arg', async () => {
const lines = await run(
'--names "foo|bar" --name-separator "|" "echo foo" "echo bar"',
).getLogLines();
expect(lines).toContainEqual(expect.stringContaining('[foo] foo'));
expect(lines).toContainEqual(expect.stringContaining('[bar] bar'));
});
});
describe('specifies custom prefix', () => {
it.each(['--prefix', '-p'])('%s', async (arg) => {
const lines = await run(`${arg} command "echo foo" "echo bar"`).getLogLines();
expect(lines).toContainEqual(expect.stringContaining('[echo foo] foo'));
expect(lines).toContainEqual(expect.stringContaining('[echo bar] bar'));
});
});
describe('specifies custom prefix length', () => {
it.each(['--prefix command --prefix-length 5', '-p command -l 5'])('%s', async (arg) => {
const lines = await run(`${arg} "echo foo" "echo bar"`).getLogLines();
expect(lines).toContainEqual(expect.stringContaining('[ec..o] foo'));
expect(lines).toContainEqual(expect.stringContaining('[ec..r] bar'));
});
});
describe('--pad-prefix', () => {
it('pads prefixes with spaces', async () => {
const lines = await run('--pad-prefix -n foo,barbaz "echo foo" "echo bar"').getLogLines();
expect(lines).toContainEqual(expect.stringContaining('[foo ]'));
expect(lines).toContainEqual(expect.stringContaining('[barbaz]'));
});
});
describe('--restart-tries', () => {
it('changes how many times a command will restart', async () => {
const lines = await run('--restart-tries 1 "exit 1"').getLogLines();
expect(lines).toEqual([
expect.stringContaining('[0] exit 1 exited with code 1'),
expect.stringContaining('[0] exit 1 restarted'),
expect.stringContaining('[0] exit 1 exited with code 1'),
]);
});
});
describe('--kill-others', () => {
describe('kills on success', () => {
it.each(['--kill-others', '-k'])('%s', async (arg) => {
const lines = await run(
`${arg} "node __fixtures__/sleep.js 10" "exit 0"`,
).getLogLines();
expect(lines).toContainEqual(expect.stringContaining('[1] exit 0 exited with code 0'));
expect(lines).toContainEqual(
expect.stringContaining('Sending SIGTERM to other processes'),
);
expect(lines).toContainEqual(
expect.stringMatching(
createKillMessage('[0] node __fixtures__/sleep.js 10', 'SIGTERM'),
),
);
});
});
it('kills on failure', async () => {
const lines = await run(
'--kill-others "node __fixtures__/sleep.js 10" "exit 1"',
).getLogLines();
expect(lines).toContainEqual(expect.stringContaining('[1] exit 1 exited with code 1'));
expect(lines).toContainEqual(expect.stringContaining('Sending SIGTERM to other processes'));
expect(lines).toContainEqual(
expect.stringMatching(
createKillMessage('[0] node __fixtures__/sleep.js 10', 'SIGTERM'),
),
);
});
});
describe('--kill-others-on-fail', () => {
it('does not kill on success', async () => {
const lines = await run(
'--kill-others-on-fail "node __fixtures__/sleep.js 0.5" "exit 0"',
).getLogLines();
expect(lines).toContainEqual(expect.stringContaining('[1] exit 0 exited with code 0'));
expect(lines).toContainEqual(
expect.stringContaining('[0] node __fixtures__/sleep.js 0.5 exited with code 0'),
);
});
it('kills on failure', async () => {
const lines = await run(
'--kill-others-on-fail "node __fixtures__/sleep.js 10" "exit 1"',
).getLogLines();
expect(lines).toContainEqual(expect.stringContaining('[1] exit 1 exited with code 1'));
expect(lines).toContainEqual(expect.stringContaining('Sending SIGTERM to other processes'));
expect(lines).toContainEqual(
expect.stringMatching(
createKillMessage('[0] node __fixtures__/sleep.js 10', 'SIGTERM'),
),
);
});
});
describe('--handle-input', () => {
describe('forwards input to first process by default', () => {
it.each(['--handle-input', '-i'])('%s', async (arg) => {
const child = run(`${arg} "node __fixtures__/read-echo.js"`);
child.log.subscribe((line) => {
if (/READING/.test(line)) {
child.stdin.write('stop\n');
}
});
const lines = await child.getLogLines();
const exit = await child.exit;
expect(exit.code).toBe(0);
expect(lines).toContainEqual(expect.stringContaining('[0] stop'));
expect(lines).toContainEqual(
expect.stringContaining('[0] node __fixtures__/read-echo.js exited with code 0'),
);
});
});
it('forwards input to process --default-input-target', async () => {
const child = run(
'-ki --default-input-target 1 "node __fixtures__/read-echo.js" "node __fixtures__/read-echo.js"',
);
child.log.subscribe((line) => {
if (/\[1\] READING/.test(line)) {
child.stdin.write('stop\n');
}
});
const lines = await child.getLogLines();
const exit = await child.exit;
expect(exit.code).toBeGreaterThan(0);
expect(lines).toContainEqual(expect.stringContaining('[1] stop'));
expect(lines).toContainEqual(
expect.stringMatching(
createKillMessage('[0] node __fixtures__/read-echo.js', 'SIGTERM'),
),
);
});
it('forwards input to specified process', async () => {
const child = run('-ki "node __fixtures__/read-echo.js" "node __fixtures__/read-echo.js"');
child.log.subscribe((line) => {
if (/\[1\] READING/.test(line)) {
child.stdin.write('1:stop\n');
}
});
const lines = await child.getLogLines();
const exit = await child.exit;
expect(exit.code).toBeGreaterThan(0);
expect(lines).toContainEqual(expect.stringContaining('[1] stop'));
expect(lines).toContainEqual(
expect.stringMatching(
createKillMessage('[0] node __fixtures__/read-echo.js', 'SIGTERM'),
),
);
});
});
describe('--teardown', () => {
it('runs teardown commands when input commands exit', async () => {
const lines = await run('--teardown "echo bye" "echo hey"').getLogLines();
expect(lines).toEqual([
expect.stringContaining('[0] hey'),
expect.stringContaining('[0] echo hey exited with code 0'),
expect.stringContaining('--> Running teardown command "echo bye"'),
expect.stringContaining('bye'),
expect.stringContaining('--> Teardown command "echo bye" exited with code 0'),
]);
});
it('runs multiple teardown commands', async () => {
const lines = await run(
'--teardown "echo bye" --teardown "echo bye2" "echo hey"',
).getLogLines();
expect(lines).toContain('bye');
expect(lines).toContain('bye2');
});
});
describe('--timings', () => {
const defaultTimestampFormatRegex = /\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{3}/;
const tableTopBorderRegex = /^--> ┌[─┬]+┐$/;
const tableHeaderRowRegex = /^--> │ name +│ duration +│ exit code +│ killed +│ command +│$/;
const tableBottomBorderRegex = /^--> └[─┴]+┘$/;
const timingsTests = {
'shows timings on success': ['node __fixtures__/sleep.js 0.5', 'exit 0'],
'shows timings on failure': ['node __fixtures__/sleep.js 0.75', 'exit 1'],
};
it.each(Object.entries(timingsTests))('%s', async (_, commands) => {
const lines = await run(
`--timings ${commands.map((command) => `"${command}"`).join(' ')}`,
).getLogLines();
// Expect output to contain process start / stop messages for each command
commands.forEach((command, index) => {
const escapedCommand = escapeRegExp(command);
expect(lines).toContainEqual(
expect.stringMatching(
new RegExp(
`^\\[${index}] ${escapedCommand} started at ${defaultTimestampFormatRegex.source}$`,
),
),
);
expect(lines).toContainEqual(
expect.stringMatching(
new RegExp(
`^\\[${index}] ${escapedCommand} stopped at ${defaultTimestampFormatRegex.source} after (\\d|,)+ms$`,
),
),
);
});
// Expect output to contain timings table
expect(lines).toContainEqual(expect.stringMatching(tableTopBorderRegex));
expect(lines).toContainEqual(expect.stringMatching(tableHeaderRowRegex));
expect(lines).toContainEqual(expect.stringMatching(tableBottomBorderRegex));
});
});
describe('--passthrough-arguments', () => {
it('argument placeholders are properly replaced when passthrough-arguments is enabled', async () => {
const lines = await run('--passthrough-arguments "echo {1}" -- echo').getLogLines();
expect(lines).toContainEqual(expect.stringContaining('[0] echo echo exited with code 0'));
});
it('argument placeholders are not replaced when passthrough-arguments is disabled', async () => {
const lines = await run('"echo {1}" -- echo').getLogLines();
expect(lines).toContainEqual(expect.stringContaining('[0] echo {1} exited with code 0'));
expect(lines).toContainEqual(expect.stringContaining('[1] echo exited with code 0'));
});
});
================================================
FILE: bin/index.ts
================================================
#!/usr/bin/env node
import process from 'node:process';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import { assertDeprecated } from '../lib/assert.js';
import * as defaults from '../lib/defaults.js';
import { concurrently } from '../lib/index.js';
import { castArray } from '../lib/utils.js';
import { readPackageJson } from './read-package-json.js';
const version = String(readPackageJson().version);
const epilogue = `For documentation and more examples, visit:\nhttps://github.com/open-cli-tools/concurrently/tree/v${version}/docs`;
// Clean-up arguments (yargs expects only the arguments after the program name)
const program = yargs(hideBin(process.argv))
.parserConfiguration({
// Avoids options that can be specified multiple times from requiring a `--` to pass commands
'greedy-arrays': false,
// Makes sure that --passthrough-arguments works correctly
'populate--': true,
})
.usage('$0 [options] ')
.help('h')
.alias('h', 'help')
.version(version)
.alias('version', 'v')
.alias('version', 'V')
// TODO: Add some tests for this.
.env('CONCURRENTLY')
.options({
// General
'max-processes': {
alias: 'm',
describe:
'How many processes should run at once.\n' +
'New processes only spawn after all restart tries of a process.\n' +
'Exact number or a percent of CPUs available (for example "50%")',
type: 'string',
},
names: {
alias: 'n',
describe:
'List of custom names to be used in prefix template.\n' +
'Example names: "main,browser,server"',
type: 'string',
},
'name-separator': {
describe:
'The character to split on. Example usage:\n' +
'-n "styles|scripts|server" --name-separator "|"',
default: defaults.nameSeparator,
},
success: {
alias: 's',
describe:
'Which command(s) must exit with code 0 in order for concurrently exit with ' +
'code 0 too. Options are:\n' +
'- "first" for the first command to exit;\n' +
'- "last" for the last command to exit;\n' +
'- "all" for all commands;\n' +
// Note: not a typo. Multiple commands can have the same name.
'- "command-{name}"/"command-{index}" for the commands with that name or index;\n' +
'- "!command-{name}"/"!command-{index}" for all commands but the ones with that ' +
'name or index.\n',
default: defaults.success,
},
raw: {
alias: 'r',
describe:
'Output only raw output of processes, disables prettifying ' +
'and concurrently coloring.',
type: 'boolean',
},
// This one is provided for free. Chalk reads this itself and removes colors.
// https://www.npmjs.com/package/chalk#supportscolor
'no-color': {
describe: 'Disables colors from logging',
type: 'boolean',
},
hide: {
describe:
'Comma-separated list of processes to hide the output.\n' +
'The processes can be identified by their name or index.',
default: defaults.hide,
type: 'string',
},
group: {
alias: 'g',
describe: 'Order the output as if the commands were run sequentially.',
type: 'boolean',
},
timings: {
describe: 'Show timing information for all processes.',
type: 'boolean',
default: defaults.timings,
},
'passthrough-arguments': {
alias: 'P',
describe:
'Passthrough additional arguments to commands (accessible via placeholders) ' +
'instead of treating them as commands.',
type: 'boolean',
default: defaults.passthroughArguments,
},
teardown: {
describe:
'Clean up command(s) to execute before exiting concurrently. Might be specified multiple times.\n' +
"These aren't prefixed and they don't affect concurrently's exit code.",
type: 'string',
array: true,
},
// Kill others
'kill-others': {
alias: 'k',
describe: 'Kill other processes once the first exits.',
type: 'boolean',
},
'kill-others-on-fail': {
describe: 'Kill other processes if one exits with non zero status code.',
type: 'boolean',
},
'kill-signal': {
alias: 'ks',
describe:
'Signal to send to other processes if one exits or dies. (SIGTERM/SIGKILL, defaults to SIGTERM)',
type: 'string',
default: defaults.killSignal,
},
'kill-timeout': {
describe: 'How many milliseconds to wait before forcing process terminating.',
type: 'number',
},
// Prefix
prefix: {
alias: 'p',
describe:
'Prefix used in logging for each process.\n' +
'Possible values: index, pid, time, command, name, none, or a template. ' +
'Example template: "{time}-{pid}"',
defaultDescription: 'index or name (when --names is set)',
type: 'string',
},
'prefix-colors': {
alias: 'c',
describe:
'Comma-separated list of Chalk colors to use on prefixes. ' +
'If there are more commands than colors, the last color will be repeated.\n' +
'- Available modifiers: reset, bold, dim, italic, underline, inverse, hidden, strikethrough\n' +
'- Available colors: black, red, green, yellow, blue, magenta, cyan, white, gray, \n' +
'any hex values for colors (e.g. #23de43) or auto for an automatically picked color\n' +
'- Available background colors: bgBlack, bgRed, bgGreen, bgYellow, bgBlue, bgMagenta, bgCyan, bgWhite\n' +
'See https://www.npmjs.com/package/chalk for more information.',
default: defaults.prefixColors,
type: 'string',
},
'prefix-length': {
alias: 'l',
describe:
'Limit how many characters of the command is displayed in prefix. ' +
'The option can be used to shorten the prefix when it is set to "command"',
default: defaults.prefixLength,
type: 'number',
},
'pad-prefix': {
describe: 'Pads short prefixes with spaces so that the length of all prefixes match',
type: 'boolean',
},
'timestamp-format': {
alias: 't',
describe:
'Specify the timestamp in Unicode format:\n' +
'https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table',
default: defaults.timestampFormat,
type: 'string',
},
// Restarting
'restart-tries': {
describe:
'How many times a process that died should restart.\n' +
'Negative numbers will make the process restart forever.',
default: defaults.restartTries,
type: 'number',
},
'restart-after': {
describe: 'Delay before restarting the process, in milliseconds, or "exponential".',
default: defaults.restartDelay,
type: 'string',
},
// Input
'handle-input': {
alias: 'i',
describe:
'Whether input should be forwarded to the child processes. ' +
'See examples for more information.',
type: 'boolean',
},
'default-input-target': {
default: defaults.defaultInputTarget,
describe:
'Identifier for child process to which input on stdin ' +
'should be sent if not specified at start of input.\n' +
'Can be either the index or the name of the process.',
},
})
.group(
['m', 'n', 'name-separator', 's', 'r', 'no-color', 'hide', 'g', 'timings', 'P', 'teardown'],
'General',
)
.group(['p', 'c', 'l', 't', 'pad-prefix'], 'Prefix styling')
.group(['i', 'default-input-target'], 'Input handling')
.group(['k', 'kill-others-on-fail', 'kill-signal', 'kill-timeout'], 'Killing other processes')
.group(['restart-tries', 'restart-after'], 'Restarting')
.epilogue(epilogue);
const args = program.parseSync();
assertDeprecated(
args.nameSeparator === defaults.nameSeparator,
'name-separator',
'Use commas as name separators instead.',
);
// Get names of commands by the specified separator
const names = (args.names || '').split(args.nameSeparator);
const additionalArguments = castArray(args['--'] ?? []).map(String);
const commands = args.passthroughArguments ? args._ : args._.concat(additionalArguments);
if (!commands.length) {
program.showHelp();
process.exit();
}
concurrently(
commands.map((command, index) => ({
command: String(command),
name: names[index],
})),
{
handleInput: args.handleInput,
defaultInputTarget: args.defaultInputTarget,
killOthersOn: args.killOthers
? ['success', 'failure']
: args.killOthersOnFail
? ['failure']
: [],
killSignal: args.killSignal,
killTimeout: args.killTimeout,
maxProcesses: args.maxProcesses,
raw: args.raw,
hide: args.hide.split(','),
group: args.group,
prefix: args.prefix,
prefixColors: args.prefixColors.split(','),
prefixLength: args.prefixLength,
padPrefix: args.padPrefix,
restartDelay:
args.restartAfter === 'exponential' ? 'exponential' : Number(args.restartAfter),
restartTries: args.restartTries,
successCondition: args.success,
timestampFormat: args.timestampFormat,
timings: args.timings,
teardown: args.teardown,
additionalArguments: args.passthroughArguments ? additionalArguments : undefined,
},
).result.then(
() => process.exit(0),
() => process.exit(1),
);
================================================
FILE: bin/read-package-json.ts
================================================
import { readFileSync } from 'node:fs';
import { createRequire } from 'node:module';
/**
* Read the package.json file of `concurrently`
*/
export function readPackageJson(): Record {
let resolver;
try {
resolver = require.resolve;
} catch {
resolver = createRequire(import.meta.url).resolve;
}
const path = resolver('concurrently/package.json');
const content = readFileSync(path, 'utf8');
return JSON.parse(content);
}
================================================
FILE: docs/README.md
================================================
# Concurrently Documentation
## CLI
These articles cover using concurrently through CLI:
- [Prefixing](./cli/prefixing.md)
- [Output Control](./cli/output-control.md)
- [Success Conditions](./cli/success.md)
- [Shortcuts](./cli/shortcuts.md)
- [Terminating Commands](./cli/terminating.md)
- [Restarting Commands](./cli/restarting.md)
- [Input Handling](./cli/input-handling.md)
- [Passthrough Arguments](./cli/passthrough-arguments.md)
- [Configuration](./cli/configuration.md)
================================================
FILE: docs/cli/configuration.md
================================================
# Configuration
You might want to configure concurrently to always have certain flags on.
Any of concurrently's flags can be set via environment variables that are prefixed with `CONCURRENTLY_`.
```bash
$ export CONCURRENTLY_KILL_OTHERS=true
$ export CONCURRENTLY_HANDLE_INPUT=true
# Equivalent to passing --kill-others and --handle-input
$ concurrently nodemon "echo 'hey nodemon, you won't last long'"
```
================================================
FILE: docs/cli/input-handling.md
================================================
# Input Handling
By default, concurrently doesn't send input to any commands it spawns.
In the below example, typing `rs` to manually restart [nodemon](https://nodemon.io/) does nothing:
```bash
$ concurrently 'nodemon' 'npm run watch-js'
rs
```
To turn on input handling, it's necessary to set the `--handle-input`/`-i` flag.
This will send `rs` to the first command:
```bash
$ concurrently --handle-input 'nodemon' 'npm run watch-js'
rs
```
To send input to a different command instead, it's possible to prefix the input with the command index, followed by a `:`.
For example, the below sends `rs` to the second command:
```bash
$ concurrently --handle-input 'npm run watch-js' 'nodemon'
1:rs
```
If the command has a name, it's also possible to target it using that command's name:
```bash
$ concurrently --handle-input --names js,server 'npm run watch-js' 'nodemon'
server:rs
```
It's also possible to change the default command that receives input.
To do this, set the `--default-input-target` flag to a command's index or name.
```bash
$ concurrently --handle-input --default-input-target 1 'npm run watch-js' 'nodemon'
rs
```
================================================
FILE: docs/cli/output-control.md
================================================
# Output Control
concurrently offers a few ways to control a command's output.
## Hiding
A command's outputs (and all its events) can be hidden by using the `--hide` flag.
```bash
$ concurrently --hide 0 'echo Hello there' 'echo General Kenobi!'
[1] General Kenobi!
[1] echo 'General Kenobi!' exited with code 0
```
## Grouping
It might be useful at times to make sure that the commands outputs are grouped together, while running them in parallel.
This can be done with the `--group` flag.
```bash
$ concurrently --group 'echo Hello there && sleep 2 && echo General Kenobi!' 'echo hi Star Wars fans'
[0] Hello there
[0] General Kenobi!
[0] echo Hello there && sleep 2 && echo 'General Kenobi!' exited with code 0
[1] hi Star Wars fans
[1] echo hi Star Wars fans exited with code 0
```
## No Colors
When piping concurrently's outputs to another command or file, you might want to force it to not use colors, as these can break the other command's parsing, or reduce the legibility of the output in non-terminal environments.
```bash
$ concurrently -c red,blue --no-color 'echo Hello there' 'echo General Kenobi!'
```
================================================
FILE: docs/cli/passthrough-arguments.md
================================================
# Passthrough Arguments
If you have a shortcut for running a specific combination of commands through concurrently,
you might need at some point to pass additional arguments/flags to some of these.
For example, imagine you have in your `package.json` file scripts like this:
```jsonc
{
// ...
"scripts": {
"build:client": "tsc -p client",
"build:server": "tsc -p server",
"build": "concurrently npm:build:client npm:build:server",
},
}
```
If you wanted to run only either `build:server` or `build:client` with an additional `--noEmit` flag,
you can do so with `npm run build:server -- --noEmit`, for example.
However, if you want to do that while using concurrently, as `npm run build -- --noEmit` for example,
you might find that concurrently actually parses `--noEmit` as its own flag, which does nothing,
because it doesn't exist.
To solve this, you can set the `--passthrough-arguments`/`-P` flag, which instructs concurrently to
take everything after a `--` as additional arguments that are passed through to the input commands
via a few placeholder styles:
## Single argument
We can modify the original `build` script to pass a single additional argument/flag to a script by using
a 1-indexed `{number}` placeholder to the command you want it to apply to:
```jsonc
{
// ...
"scripts": {
// ...
"build": "concurrently -P 'npm:build:client -- {1}' npm:build:server --",
"typecheck": "npm run build -- --noEmit",
},
}
```
With this, running `npm run typecheck` will pass `--noEmit` only to `npm run build:client`.
## All arguments
In the original `build` example script, you're more likely to want to pass every additional argument/flag
to your commands. This can be done with the `{@}` placeholder.
```jsonc
{
// ...
"scripts": {
// ...
"build": "concurrently -P 'npm:build:client -- {@}' 'npm:build:server -- {@}' --",
"typecheck": "npm run build -- --watch --noEmit",
},
}
```
In the above example, both `--watch` and `--noEmit` are passed to each command.
## All arguments, combined
If for some reason you wish to combine all additional arguments into a single one, you can do that with the `{*}` placeholder,
which wraps the arguments in quotes.
```jsonc
{
// ...
"scripts": {
// ...
"build": "concurrently -P 'npm:build:client -- --outDir {*}/client' 'npm:build:server -- --outDir {*}/server' -- $(date)",
},
}
```
In the above example, the output of the `date` command, which looks like `Sun 1 Sep 2024 23:50:00 AEST` will be passed as a single string to the `--outDir` parameter of both commands.
================================================
FILE: docs/cli/prefixing.md
================================================
# Prefixing
## Prefix Styles
concurrently will by default prefix each command's outputs with a zero-based index, wrapped in square brackets:
```bash
$ concurrently 'echo Hello there' "echo 'General Kenobi!'"
[0] Hello there
[1] General Kenobi!
[0] echo Hello there exited with code 0
[1] echo 'General Kenobi!' exited with code 0
```
If you've given the commands names, they are used instead:
```bash
$ concurrently --names one,two 'echo Hello there' "echo 'General Kenobi!'"
[one] Hello there
[two] General Kenobi!
[one] echo Hello there exited with code 0
[two] echo 'General Kenobi!' exited with code 0
```
There are other prefix styles available too:
| Style | Description |
| --------- | --------------------------------- |
| `index` | Zero-based command's index |
| `name` | The command's name |
| `command` | The command's line |
| `time` | Time of output |
| `pid` | ID of the command's process (PID) |
| `none` | No prefix |
Any of these can be used by setting the `--prefix`/`-p` flag. For example:
```bash
$ concurrently --prefix pid 'echo Hello there' 'echo General Kenobi!'
[2222] Hello there
[2223] General Kenobi!
[2222] echo Hello there exited with code 0
[2223] echo 'General Kenobi!' exited with code 0
```
It's also possible to have a prefix based on a template. Any of the styles listed above can be used by wrapping it in `{}`.
Doing so will also remove the square brackets:
```bash
$ concurrently --prefix '{index}-{pid}' 'echo Hello there' 'echo General Kenobi!'
0-2222 Hello there
1-2223 General Kenobi!
0-2222 echo Hello there exited with code 0
1-2223 echo 'General Kenobi!' exited with code 0
```
## Prefix Colors
By default, there are no colors applied to concurrently prefixes, and they just use whatever the terminal's defaults are.
This can be changed by using the `--prefix-colors`/`-c` flag, which takes a comma-separated list of colors to use.
The available values are color names (e.g. `green`, `magenta`, `gray`, etc), a hex value (such as `#23de43`), or `auto`, to automatically select a color.
```bash
$ concurrently -c red,blue 'echo Hello there' 'echo General Kenobi!'
```
List of available color names
- `black`
- `blue`
- `cyan`
- `green`
- `gray`
- `magenta`
- `red`
- `white`
- `yellow`
Colors can take modifiers too. Several can be applied at once by appending `..` and so on.
```bash
$ concurrently -c '#23de43.inverse,bold.blue.dim' 'echo Hello there' 'echo General Kenobi!'
```
List of available modifiers
- `reset`
- `bold`
- `dim`
- `hidden`
- `inverse`
- `italic`
- `strikethrough`
- `underline`
A background color can be set in a similarly fashion.
```bash
$ concurrently -c bgGray,red.bgBlack 'echo Hello there' 'echo General Kenobi!'
```
List of available background color names
- `bgBlack`
- `bgBlue`
- `bgCyan`
- `bgGreen`
- `bgGray`
- `bgMagenta`
- `bgRed`
- `bgWhite`
- `bgYellow`
## Prefix Length
When using the `command` prefix style, it's possible that it'll be too long.
It can be limited by setting the `--prefix-length`/`-l` flag:
```bash
$ concurrently -p command -l 10 'echo Hello there' 'echo General Kenobi!'
[echo..here] Hello there
[echo..bi!'] General Kenobi!
[echo..here] echo Hello there exited with code 0
[echo..bi!'] echo 'General Kenobi!' exited with code 0
```
It's also possible that some prefixes are too short, and you want all of them to have the same length.
This can be done by setting the `--pad-prefix` flag:
```bash
$ concurrently -n foo,barbaz --pad-prefix 'echo Hello there' 'echo General Kenobi!'
[foo ] Hello there
[foo ] echo Hello there exited with code 0
[barbaz] General Kenobi!
[barbaz] echo 'General Kenobi!' exited with code 0
```
> [!NOTE]
> If using the `pid` prefix style in combination with [`--restart-tries`](./restarting.md), the length of the PID might grow, in which case all subsequent lines will match the new length.
> This might happen, for example, if the PID was 99 and it's now 100.
================================================
FILE: docs/cli/restarting.md
================================================
# Restarting Commands
Sometimes it's useful to have commands that exited with a non-zero status to restart automatically.
concurrently lets you configure how many times you wish for such a command to restart through the `--restart-tries` flag:
```bash
$ concurrently --restart-tries 2 'exit 1'
[0] exit 1 exited with code 1
[0] exit 1 restarted
[0] exit 1 exited with code 1
[0] exit 1 restarted
[0] exit 1 exited with code 1
```
Sometimes, it might be interesting to have commands wait before restarting.
To do this, simply set `--restart-after` to a the number of milliseconds you'd like to delay restarting.
```bash
$ concurrently -p time --restart-tries 1 --restart-after 3000 'exit 1'
[2024-09-01 23:43:55.871] exit 1 exited with code 1
[2024-09-01 23:43:58.874] exit 1 restarted
[2024-09-01 23:43:58.891] exit 1 exited with code 1
```
If a command is not having success spawning, you might want to instead apply an exponential back-off.
Set `--restart-after exponential` to have commands respawn with a `2^N` seconds delay.
```bash
$ concurrently -p time --restart-tries 3 --restart-after exponential 'exit 1'
[2024-09-01 23:49:01.124] exit 1 exited with code 1
[2024-09-01 23:49:02.127] exit 1 restarted
[2024-09-01 23:49:02.139] exit 1 exited with code 1
[2024-09-01 23:49:04.141] exit 1 restarted
[2024-09-01 23:49:04.157] exit 1 exited with code 1
[2024-09-01 23:49:08.158] exit 1 restarted
[2024-09-01 23:49:08.174] exit 1 exited with code 1
```
================================================
FILE: docs/cli/shortcuts.md
================================================
# Command Shortcuts
Package managers that execute scripts from a `package.json` or `deno.(json|jsonc)` file can be shortened when in concurrently.
The following are supported:
| Syntax | Expands to |
| --------------- | --------------------- |
| `npm: