[
  {
    "path": ".editorconfig",
    "content": "# EditorConfig is awesome: https://editorconfig.org\n\nroot = true\n\n[*]\nend_of_line = lf\ninsert_final_newline = true\ncharset = utf-8\nindent_style = space\ntrim_trailing_whitespace = true\n\n[*.{js,ts}]\nindent_size = 4\nmax_line_length = 100\n\n[*.md]\ntrim_trailing_whitespace = false\n"
  },
  {
    "path": ".gitattributes",
    "content": "* text=auto eol=lf\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# Thank you! <3\n\ngithub: [gustavohenke, paescuj]\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.yml",
    "content": "name: Bug Report\ndescription: File a bug report\nbody:\n  - type: textarea\n    attributes:\n      label: Describe the bug\n      value: |\n        <!--\n\n        >> Before getting started, please ensure there's no existing issue with the same concern!\n\n        Please provide the following information:\n\n        - Description: Clear and concise description of what the bug is\n        - Expected Behavior: What you expected to happen\n        - Environment: At least the OS, Node.js, and concurrently's versions\n        - Reproduction: A way to reproduce the issue\n\n        Our time to work on this project is limited.\n        With an accurate report you're helping us to understand and solve the request faster.\n        Please understand that reports that do not meet the requirements may be closed without further notice.\n\n        Thank you!\n\n        -->\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature-request.yml",
    "content": "name: Feature Request\ndescription: Open a feature request\nbody:\n  - type: textarea\n    attributes:\n      label: Describe the feature request\n      value: |\n        <!--\n\n        >> Before getting started, please ensure there's no existing issue with the same concern!\n\n        Please provide the following information:\n\n        - Description: Clear and concise description of what you want\n        - Use Case: Why is this needed and what is a use case for it\n\n        Our time to work on this project is limited.\n        With an accurate report you're helping us to understand and solve the request faster.\n        Please understand that reports that do not meet the requirements may be closed without further notice.\n\n        Thank you!\n\n        -->\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/actions/setup/action.yml",
    "content": "name: Setup\ndescription: Setup the environment for the project\n\ninputs:\n  node-version:\n    description: Node.js version\n    required: false\n  node-registry:\n    description: Node.js package registry to set up for auth\n    required: false\n\nruns:\n  using: composite\n  steps:\n    - name: Install Node.js\n      uses: actions/setup-node@v5\n      with:\n        node-version-file: ${{ !inputs.node-version && '.node-version' || '' }}\n        node-version: ${{ inputs.node-version }}\n        registry-url: ${{ inputs.node-registry }}\n        package-manager-cache: false\n\n    - name: Install pnpm\n      uses: pnpm/action-setup@v4\n\n    - name: Get pnpm store directory\n      id: pnpm-cache-dir\n      shell: bash\n      run: echo \"dir=$(pnpm store path)\" >> $GITHUB_OUTPUT\n\n    - name: Setup pnpm cache\n      uses: actions/cache@v4\n      with:\n        path: ${{ steps.pnpm-cache-dir.outputs.dir }}\n        key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}\n        restore-keys: |\n          ${{ runner.os }}-pnpm-store-\n\n    - name: Install dependencies\n      shell: bash\n      run: pnpm install\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n    branches:\n      - main\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  lint:\n    name: Lint\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v5\n\n      - name: Setup\n        uses: ./.github/actions/setup\n\n      - name: Lint\n        run: pnpm run lint\n\n      - name: Format\n        run: pnpm run format\n\n      - name: Typecheck\n        run: pnpm run typecheck\n\n  test:\n    name: Test (Node.js ${{ matrix.node }}, ${{ matrix.os.name }})\n    runs-on: ${{ matrix.os.version }}\n    strategy:\n      fail-fast: false\n      matrix:\n        node:\n          - 20\n          - 22\n          - 24\n        os:\n          - name: Ubuntu\n            version: ubuntu-latest\n          - name: Windows\n            version: windows-latest\n          - name: macOS\n            version: macOS-latest\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v5\n\n      - name: Setup\n        uses: ./.github/actions/setup\n        with:\n          node-version: ${{ matrix.node }}\n\n      - name: Test\n        run: pnpm exec vitest --coverage\n\n      - name: Submit coverage\n        uses: coverallsapp/github-action@master\n        continue-on-error: true\n        with:\n          github-token: ${{ secrets.github_token }}\n          flag-name: Node.js ${{ matrix.node }} on ${{ matrix.os.name }}\n          parallel: true\n\n  coverage:\n    name: Coverage\n    needs: test\n    runs-on: ubuntu-latest\n    continue-on-error: true\n    steps:\n      - name: Finish coverage\n        uses: coverallsapp/github-action@master\n        with:\n          github-token: ${{ secrets.github_token }}\n          parallel-finished: true\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\non:\n  push:\n    tags:\n      - 'v*'\n\njobs:\n  gh-release:\n    name: Create GitHub Release\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v5\n\n      - name: Create release\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: gh release create \"$GITHUB_REF_NAME\" --generate-notes\n\n  publish-npm:\n    name: Publish to NPM\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      id-token: write\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v5\n\n      - name: Setup\n        uses: ./.github/actions/setup\n        with:\n          node-registry: https://registry.npmjs.org\n\n      - name: Publish to NPM\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}\n          NPM_CONFIG_PROVENANCE: true\n        run: pnpm publish --no-git-checks\n"
  },
  {
    "path": ".gitignore",
    "content": "# Outputs\ndist\n\n# Logs\n*.log\n\n# Coverage directory used by tools like istanbul\ncoverage\n\n# Dependency directory\nnode_modules\n\n# OS X\n.DS_Store\n"
  },
  {
    "path": ".husky/pre-commit",
    "content": "./node_modules/.bin/lint-staged\n"
  },
  {
    "path": ".node-version",
    "content": "22\n"
  },
  {
    "path": ".prettierignore",
    "content": "dist\ncoverage\npnpm-lock.yaml\n"
  },
  {
    "path": ".prettierrc.json",
    "content": "{\n  \"singleQuote\": true\n}\n"
  },
  {
    "path": ".vscode/extensions.json",
    "content": "{\n  \"recommendations\": [\n    \"dbaeumer.vscode-eslint\",\n    \"esbenp.prettier-vscode\",\n    \"editorconfig.editorconfig\",\n    \"vitest.explorer\"\n  ]\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  // For contributors with the Jest extension installed,\n  // it might incorrectly report errors in the suite\n  \"jest.enable\": false,\n  \"editor.codeActionsOnSave\": {\n    \"source.fixAll.eslint\": \"explicit\"\n  },\n  \"editor.defaultFormatter\": \"esbenp.prettier-vscode\",\n  \"editor.formatOnSave\": true,\n  \"[javascript, typescript]\": {\n    \"editor.formatOnSave\": false\n  },\n  \"[json]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  },\n  \"editor.rulers\": [100]\n}\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\nPull requests and contributions are warmly welcome.\nPlease follow existing code style and commit message conventions. Also remember to keep documentation\nupdated.\n\n**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.\n\n# Maintaining\n\n## Code Format & Linting\n\nCode 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).\n\nIn case of problems, a corresponding message is displayed in your terminal.\nPlease fix them and then run the commit command again.\n\n## Test\n\nTests can be executed with the following command:\n\n```bash\npnpm test\n```\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2015 Kimmo Brunfeldt\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# concurrently\n\n[![Latest Release](https://img.shields.io/github/v/release/open-cli-tools/concurrently?label=Release)](https://github.com/open-cli-tools/concurrently/releases)\n[![License](https://img.shields.io/github/license/open-cli-tools/concurrently?label=License)](https://github.com/open-cli-tools/concurrently/blob/main/LICENSE)\n[![Weekly Downloads on NPM](https://img.shields.io/npm/dw/concurrently?label=Downloads&logo=npm)](https://www.npmjs.com/package/concurrently)\n[![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)\n[![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)\n\nRun multiple commands concurrently.\nLike `npm run watch-js & npm run watch-less` but better.\n\n![Demo](docs/demo.gif)\n\n**Table of Contents**\n\n- [concurrently](#concurrently)\n  - [Why](#why)\n  - [Installation](#installation)\n  - [Usage](#usage)\n  - [API](#api)\n    - [`concurrently(commands[, options])`](#concurrentlycommands-options)\n    - [`Command`](#command)\n    - [`CloseEvent`](#closeevent)\n  - [FAQ](#faq)\n\n## Why\n\nI like [task automation with npm](https://web.archive.org/web/20220531064025/https://github.com/substack/blog/blob/master/npm_run.markdown)\nbut the usual way to run multiple commands concurrently is\n`npm run watch-js & npm run watch-css`. That's fine but it's hard to keep\non track of different outputs. Also if one process fails, others still keep running\nand you won't even notice the difference.\n\nAnother option would be to just run all commands in separate terminals. I got\ntired of opening terminals and made **concurrently**.\n\n**Features:**\n\n- Cross platform (including Windows)\n- Output is easy to follow with prefixes\n- With `--kill-others` switch, all commands are killed if one dies\n\n## Installation\n\n**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):\n\n|             | npm                     | Yarn                           | pnpm                       | Bun                       |\n| ----------- | ----------------------- | ------------------------------ | -------------------------- | ------------------------- |\n| **Global**  | `npm i -g concurrently` | `yarn global add concurrently` | `pnpm add -g concurrently` | `bun add -g concurrently` |\n| **Local**\\* | `npm i -D concurrently` | `yarn add -D concurrently`     | `pnpm add -D concurrently` | `bun add -d concurrently` |\n\n<sub>\\* 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.</sub>\n\n## Usage\n\n> **Note**\n> The `concurrently` command is also available under the shorthand alias `conc`.\n\nThe tool is written in Node.js, but you can use it to run **any** commands.\n\nRemember to surround separate commands with quotes:\n\n```bash\nconcurrently 'command1 arg' 'command2 arg'\n```\n\nOtherwise **concurrently** would try to run 4 separate commands:\n`command1`, `arg`, `command2`, `arg`.\n\n> [!IMPORTANT]\n> Windows only supports double quotes:\n>\n> ```bash\n> concurrently \"command1 arg\" \"command2 arg\"\n> ```\n>\n> Remember to escape the double quotes in your package.json when using Windows:\n>\n> ```json\n> \"start\": \"concurrently \\\"command1 arg\\\" \\\"command2 arg\\\"\"\n> ```\n\nYou can always check concurrently's flag list by running `concurrently --help`.\nFor the version, run `concurrently --version`.\n\nCheck out documentation and other usage examples in the [`docs` directory](./docs/README.md).\n\n## API\n\n**concurrently** can be used programmatically by using the API documented below:\n\n### `concurrently(commands[, options])`\n\n- `commands`: an array of either strings (containing the commands to run) or objects\n  with the shape `{ command, name, prefixColor, env, cwd, ipc }`.\n\n- `options` (optional): an object containing any of the below:\n  - `cwd`: the working directory to be used by all commands. Can be overridden per command.\n    Default: `process.cwd()`.\n  - `defaultInputTarget`: the default input target when reading from `inputStream`.\n    Default: `0`.\n  - `handleInput`: when `true`, reads input from `process.stdin`.\n  - `inputStream`: a [`Readable` stream](https://nodejs.org/dist/latest-v10.x/docs/api/stream.html#stream_readable_streams)\n    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`.\n  - `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)).\n  - `killOthersOn`: once the first command exits with one of these statuses, kill other commands.\n    Can be an array containing the strings `success` (status code zero) and/or `failure` (non-zero exit status).\n  - `maxProcesses`: how many processes should run at once.\n  - `outputStream`: a [`Writable` stream](https://nodejs.org/dist/latest-v10.x/docs/api/stream.html#stream_writable_streams)\n    to write logs to. Default: `process.stdout`.\n  - `prefix`: the prefix type to use when logging processes output.\n    Possible values: `index`, `pid`, `time`, `command`, `name`, `none`, or a template (eg `[{time} process: {pid}]`).\n    Default: the name of the process, or its index if no name is set.\n  - `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.\n    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.\n    Prefix colors specified per-command take precedence over this list.\n  - `prefixLength`: how many characters to show when prefixing with `command`. Default: `10`\n  - `raw`: whether raw mode should be used, meaning strictly process output will\n    be logged, without any prefixes, coloring or extra stuff. Can be overridden per command.\n  - `successCondition`: the condition to consider the run was successful.\n    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.\n    Anything else means all processes should exit successfully.\n  - `restartTries`: how many attempts to restart a process that dies will be made. Default: `0`.\n  - `restartDelay`: how many milliseconds to wait between process restarts. Default: `0`.\n  - `timestampFormat`: a [Unicode format](https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table)\n    to use when prefixing with `time`. Default: `yyyy-MM-dd HH:mm:ss.SSS`\n  - `additionalArguments`: list of additional arguments passed that will get replaced in each command. If not defined, no argument replacing will happen.\n\n> **Returns:** an object in the shape `{ result, commands }`.\n>\n> - `result`: a `Promise` that resolves if the run was successful (according to `successCondition` option),\n>   or rejects, containing an array of [`CloseEvent`](#CloseEvent), in the order that the commands terminated.\n> - `commands`: an array of all spawned [`Command`s](#Command).\n\nExample:\n\n```js\nconst concurrently = require('concurrently');\nconst { result } = concurrently(\n  [\n    'npm:watch-*',\n    { command: 'nodemon', name: 'server' },\n    { command: 'deploy', name: 'deploy', env: { PUBLIC_KEY: '...' } },\n    {\n      command: 'watch',\n      name: 'watch',\n      cwd: path.resolve(__dirname, 'scripts/watchers'),\n    },\n  ],\n  {\n    prefix: 'name',\n    killOthersOn: ['failure', 'success'],\n    restartTries: 3,\n    cwd: path.resolve(__dirname, 'scripts'),\n  },\n);\nresult.then(success, failure);\n```\n\n### `Command`\n\nAn object that contains all information about a spawned command, and ways to interact with it.<br>\nIt has the following properties:\n\n- `index`: the index of the command among all commands spawned.\n- `command`: the command line of the command.\n- `name`: the name of the command; defaults to an empty string.\n- `cwd`: the current working directory of the command.\n- `env`: an object with all the environment variables that the command will be spawned with.\n- `killed`: whether the command has been killed.\n- `state`: the command's state. Can be one of\n  - `stopped`: if the command was never started\n  - `started`: if the command is currently running\n  - `errored`: if the command failed spawning\n  - `exited`: if the command is not running anymore, e.g. it received a close event\n- `pid`: the command's process ID.\n- `stdin`: a Writable stream to the command's `stdin`.\n- `stdout`: an RxJS observable to the command's `stdout`.\n- `stderr`: an RxJS observable to the command's `stderr`.\n- `error`: an RxJS observable to the command's error events (e.g. when it fails to spawn).\n- `timer`: an RxJS observable to the command's timing events (e.g. starting, stopping).\n- `stateChange`: an RxJS observable for changes to the command's `state` property.\n- `messages`: an object with the following properties:\n  - `incoming`: an RxJS observable for the IPC messages received from the underlying process.\n  - `outgoing`: an RxJS observable for the IPC messages sent to the underlying process.\n\n  Both observables emit [`MessageEvent`](#messageevent)s.<br>\n  Note that if the command wasn't spawned with IPC support, these won't emit any values.\n\n- `close`: an RxJS observable to the command's close events.\n  See [`CloseEvent`](#CloseEvent) for more information.\n- `start()`: starts the command and sets up all of the above streams\n- `send(message[, handle, options])`: sends a message to the underlying process via IPC channels,\n  returning a promise that resolves once the message has been sent.\n  See [Node.js docs](https://nodejs.org/docs/latest/api/child_process.html#subprocesssendmessage-sendhandle-options-callback).\n- `kill([signal])`: kills the command, optionally specifying a signal (e.g. `SIGTERM`, `SIGKILL`, etc).\n\n### `MessageEvent`\n\nAn object that represents a message that was received from/sent to the underlying command process.<br>\nIt has the following properties:\n\n- `message`: the message itself.\n- `handle`: a [`net.Socket`](https://nodejs.org/docs/latest/api/net.html#class-netsocket),\n  [`net.Server`](https://nodejs.org/docs/latest/api/net.html#class-netserver) or\n  [`dgram.Socket`](https://nodejs.org/docs/latest/api/dgram.html#class-dgramsocket),\n  if one was sent, or `undefined`.\n\n### `CloseEvent`\n\nAn object with information about a command's closing event.<br>\nIt contains the following properties:\n\n- `command`: a stripped down version of [`Command`](#command), including only `name`, `command`, `env` and `cwd` properties.\n- `index`: the index of the command among all commands spawned.\n- `killed`: whether the command exited because it was killed.\n- `exitCode`: the exit code of the command's process, or the signal which it was killed with.\n- `timings`: an object in the shape `{ startDate, endDate, durationSeconds }`.\n\n## FAQ\n\n- Process exited with code _null_?\n\n  From [Node child_process documentation](http://nodejs.org/api/child_process.html#child_process_event_exit), `exit` event:\n\n  > This event is emitted after the child process ends. If the process\n  > terminated normally, code is the final exit code of the process,\n  > otherwise null. If the process terminated due to receipt of a signal,\n  > signal is the string name of the signal, otherwise null.\n\n  So _null_ means the process didn't terminate normally. This will make **concurrently**\n  to return non-zero exit code too.\n\n- Does this work with the npm-replacements [yarn](https://yarnpkg.com/), [pnpm](https://pnpm.io/), or [Bun](https://bun.sh/)?\n\n  Yes! In all examples above, you may replace \"`npm`\" with \"`yarn`\", \"`pnpm`\", or \"`bun`\".\n"
  },
  {
    "path": "bin/__fixtures__/read-echo.js",
    "content": "import process from 'node:process';\n\nprocess.stdin.on('data', (chunk) => {\n    const line = chunk.toString().trim();\n    console.log(line);\n\n    if (line === 'stop') {\n        process.exit(0);\n    }\n});\n\nconsole.log('READING');\n"
  },
  {
    "path": "bin/__fixtures__/sleep.js",
    "content": "/*\n * Platform independent implementation of 'sleep' used as a command in tests\n *\n * (Windows doesn't provide the 'sleep' command by default,\n * see https://github.com/open-cli-tools/concurrently/issues/277)\n */\nimport process from 'node:process';\n\nconst seconds = +process.argv[2];\nif (!seconds || Number.isNaN(seconds) || process.argv.length > 3) {\n    // Mimic behavior from native 'sleep' command\n    console.error('usage: sleep seconds');\n    process.exit(1);\n}\n\nawait new Promise((resolve) => setTimeout(resolve, seconds * 1000));\n"
  },
  {
    "path": "bin/index.spec.ts",
    "content": "import { spawn } from 'node:child_process';\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport readline from 'node:readline';\n\nimport { subscribeSpyTo } from '@hirez_io/observer-spy';\nimport { sendCtrlC, spawnWithWrapper } from 'ctrlc-wrapper';\nimport { build } from 'esbuild';\nimport Rx from 'rxjs';\nimport { map } from 'rxjs/operators';\nimport stringArgv from 'string-argv';\nimport { afterAll, beforeAll, describe, expect, it } from 'vitest';\n\nimport { escapeRegExp } from '../lib/utils.js';\n\nconst isWindows = process.platform === 'win32';\nconst createKillMessage = (prefix: string, signal: 'SIGTERM' | 'SIGINT' | string) => {\n    const map: Record<string, string | number> = {\n        SIGTERM: isWindows ? 1 : '(SIGTERM|143)',\n        // Could theoretically be anything (e.g. 0) if process has SIGINT handler\n        SIGINT: isWindows ? '(3221225786|0)' : '(SIGINT|130|0)',\n    };\n    return new RegExp(`${escapeRegExp(prefix)} exited with code ${map[signal] ?? signal}`);\n};\n\nlet tmpDir: string;\n\nbeforeAll(async () => {\n    // Build 'concurrently' and store it in a temporary directory\n    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'concurrently-'));\n    await build({\n        entryPoints: [path.join(__dirname, 'index.ts')],\n        platform: 'node',\n        bundle: true,\n        // it doesn't seem like esbuild is able to change a CJS module to ESM, so target CJS instead.\n        // https://github.com/evanw/esbuild/issues/1921\n        format: 'cjs',\n        outfile: path.join(tmpDir, 'concurrently.cjs'),\n    });\n    fs.copyFileSync(path.join(__dirname, '..', 'package.json'), path.join(tmpDir, 'package.json'));\n}, 8000);\n\nafterAll(() => {\n    // Remove the temporary directory where 'concurrently' was stored\n    if (tmpDir) {\n        fs.rmSync(tmpDir, { recursive: true });\n    }\n});\n\n/**\n * Creates a child process running 'concurrently' with the given args.\n * Returns observables for its combined stdout + stderr output, close events, pid, and stdin stream.\n */\nconst run = (args: string, ctrlcWrapper?: boolean) => {\n    const spawnFn = ctrlcWrapper ? spawnWithWrapper : spawn;\n    const child = spawnFn('node', [path.join(tmpDir, 'concurrently.cjs'), ...stringArgv(args)], {\n        cwd: __dirname,\n        env: {\n            ...process.env,\n        },\n    });\n\n    const stdout = readline.createInterface({\n        input: child.stdout,\n    });\n\n    const stderr = readline.createInterface({\n        input: child.stderr,\n    });\n\n    const log = new Rx.Observable<string>((observer) => {\n        stdout.on('line', (line) => {\n            observer.next(line);\n        });\n\n        stderr.on('line', (line) => {\n            observer.next(line);\n        });\n\n        child.on('close', () => {\n            observer.complete();\n        });\n    });\n\n    const exit = Rx.firstValueFrom(\n        Rx.fromEvent(child, 'exit').pipe(\n            map((event) => {\n                const exit = event as [number | null, NodeJS.Signals | null];\n                return {\n                    /** The exit code if the child exited on its own. */\n                    code: exit[0],\n                    /** The signal by which the child process was terminated. */\n                    signal: exit[1],\n                };\n            }),\n        ),\n    );\n\n    const getLogLines = async (): Promise<string[]> => {\n        const observerSpy = subscribeSpyTo(log);\n        await observerSpy.onComplete();\n        observerSpy.unsubscribe();\n        return observerSpy.getValues();\n    };\n\n    return {\n        process: child,\n        stdin: child.stdin,\n        pid: child.pid,\n        log,\n        getLogLines,\n        exit,\n    };\n};\n\nit('has help command', async () => {\n    const exit = await run('--help').exit;\n\n    expect(exit.code).toBe(0);\n});\n\nit('prints help when no arguments are passed', async () => {\n    const exit = await run('').exit;\n    expect(exit.code).toBe(0);\n});\n\ndescribe('has version command', () => {\n    const pkg = fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8');\n    const { version } = JSON.parse(pkg);\n\n    it.each(['--version', '-V', '-v'])('%s', async (arg) => {\n        const child = run(arg);\n        const log = await child.getLogLines();\n        expect(log).toContain(version);\n\n        const { code } = await child.exit;\n        expect(code).toBe(0);\n    });\n});\n\ndescribe('exiting conditions', () => {\n    it('is of success by default when running successful commands', async () => {\n        const exit = await run('\"echo foo\" \"echo bar\"').exit;\n\n        expect(exit.code).toBe(0);\n    });\n\n    it('is of failure by default when one of the command fails', async () => {\n        const exit = await run('\"echo foo\" \"exit 1\"').exit;\n\n        expect(exit.code).toBeGreaterThan(0);\n    });\n\n    it('is of success when --success=first and first command to exit succeeds', async () => {\n        const exit = await run(\n            '--success=first \"echo foo\" \"node __fixtures__/sleep.js 0.5 && exit 1\"',\n        ).exit;\n\n        expect(exit.code).toBe(0);\n    });\n\n    it('is of failure when --success=first and first command to exit fails', async () => {\n        const exit = await run(\n            '--success=first \"exit 1\" \"node __fixtures__/sleep.js 0.5 && echo foo\"',\n        ).exit;\n\n        expect(exit.code).toBeGreaterThan(0);\n    });\n\n    describe('is of success when --success=last and last command to exit succeeds', () => {\n        it.each(['--success=last', '-s last'])('%s', async (arg) => {\n            const exit = await run(`${arg} \"exit 1\" \"node __fixtures__/sleep.js 0.5 && echo foo\"`)\n                .exit;\n\n            expect(exit.code).toBe(0);\n        });\n    });\n\n    it('is of failure when --success=last and last command to exit fails', async () => {\n        const exit = await run(\n            '--success=last \"echo foo\" \"node __fixtures__/sleep.js 0.5 && exit 1\"',\n        ).exit;\n\n        expect(exit.code).toBeGreaterThan(0);\n    });\n\n    it('is of success when a SIGINT is sent', async () => {\n        // Windows doesn't support sending signals like on POSIX platforms.\n        // However, in a console, processes can be interrupted with CTRL+C (like a SIGINT).\n        // This is what we simulate here with the help of a wrapper application.\n        const child = run('\"node __fixtures__/read-echo.js\"', isWindows);\n        // Wait for command to have started before sending SIGINT\n        child.log.subscribe((line) => {\n            if (/READING/.test(line)) {\n                if (isWindows) {\n                    // Instruct the wrapper to send CTRL+C to its child\n                    sendCtrlC(child.process);\n                } else {\n                    process.kill(Number(child.pid), 'SIGINT');\n                }\n            }\n        });\n        const lines = await child.getLogLines();\n        const exit = await child.exit;\n\n        expect(exit.code).toBe(0);\n        expect(lines).toContainEqual(\n            expect.stringMatching(\n                createKillMessage(\n                    '[0] node __fixtures__/read-echo.js',\n                    // TODO: Flappy value due to race condition, sometimes killed by concurrently (exit code 1),\n                    //       sometimes terminated on its own (exit code 0).\n                    //       Related issue: https://github.com/open-cli-tools/concurrently/issues/283\n                    isWindows ? '(3221225786|0|1)' : 'SIGINT',\n                ),\n            ),\n        );\n    });\n});\n\ndescribe('does not log any extra output', () => {\n    it.each(['--raw', '-r'])('%s', async (arg) => {\n        const lines = await run(`${arg} \"echo foo\" \"echo bar\"`).getLogLines();\n\n        expect(lines).toHaveLength(2);\n        expect(lines).toContainEqual(expect.stringContaining('foo'));\n        expect(lines).toContainEqual(expect.stringContaining('bar'));\n    });\n});\n\ndescribe('--hide', () => {\n    it('hides the output of a process by its index', async () => {\n        const lines = await run('--hide 1 \"echo foo\" \"echo bar\"').getLogLines();\n\n        expect(lines).toContainEqual(expect.stringContaining('foo'));\n        expect(lines).not.toContainEqual(expect.stringContaining('bar'));\n    });\n\n    it('hides the output of a process by its name', async () => {\n        const lines = await run('-n foo,bar --hide bar \"echo foo\" \"echo bar\"').getLogLines();\n\n        expect(lines).toContainEqual(expect.stringContaining('foo'));\n        expect(lines).not.toContainEqual(expect.stringContaining('bar'));\n    });\n\n    it('hides the output of a process by its index in raw mode', async () => {\n        const lines = await run('--hide 1 --raw \"echo foo\" \"echo bar\"').getLogLines();\n\n        expect(lines).toHaveLength(1);\n        expect(lines).toContainEqual(expect.stringContaining('foo'));\n        expect(lines).not.toContainEqual(expect.stringContaining('bar'));\n    });\n\n    it('hides the output of a process by its name in raw mode', async () => {\n        const lines = await run('-n foo,bar --hide bar --raw \"echo foo\" \"echo bar\"').getLogLines();\n\n        expect(lines).toHaveLength(1);\n        expect(lines).toContainEqual(expect.stringContaining('foo'));\n        expect(lines).not.toContainEqual(expect.stringContaining('bar'));\n    });\n});\n\ndescribe('--group', () => {\n    it('groups output per process', async () => {\n        const lines = await run(\n            '--group \"echo foo && node __fixtures__/sleep.js 1 && echo bar\" \"echo baz\"',\n        ).getLogLines();\n\n        expect(lines.slice(0, 4)).toEqual([\n            expect.stringContaining('foo'),\n            expect.stringContaining('bar'),\n            expect.any(String),\n            expect.stringContaining('baz'),\n        ]);\n    });\n});\n\ndescribe('--names', () => {\n    describe('prefixes with names', () => {\n        it.each(['--names', '-n'])('%s', async (arg) => {\n            const lines = await run(`${arg} foo,bar \"echo foo\" \"echo bar\"`).getLogLines();\n\n            expect(lines).toContainEqual(expect.stringContaining('[foo] foo'));\n            expect(lines).toContainEqual(expect.stringContaining('[bar] bar'));\n        });\n    });\n\n    it('is split using --name-separator arg', async () => {\n        const lines = await run(\n            '--names \"foo|bar\" --name-separator \"|\" \"echo foo\" \"echo bar\"',\n        ).getLogLines();\n\n        expect(lines).toContainEqual(expect.stringContaining('[foo] foo'));\n        expect(lines).toContainEqual(expect.stringContaining('[bar] bar'));\n    });\n});\n\ndescribe('specifies custom prefix', () => {\n    it.each(['--prefix', '-p'])('%s', async (arg) => {\n        const lines = await run(`${arg} command \"echo foo\" \"echo bar\"`).getLogLines();\n\n        expect(lines).toContainEqual(expect.stringContaining('[echo foo] foo'));\n        expect(lines).toContainEqual(expect.stringContaining('[echo bar] bar'));\n    });\n});\n\ndescribe('specifies custom prefix length', () => {\n    it.each(['--prefix command --prefix-length 5', '-p command -l 5'])('%s', async (arg) => {\n        const lines = await run(`${arg} \"echo foo\" \"echo bar\"`).getLogLines();\n\n        expect(lines).toContainEqual(expect.stringContaining('[ec..o] foo'));\n        expect(lines).toContainEqual(expect.stringContaining('[ec..r] bar'));\n    });\n});\n\ndescribe('--pad-prefix', () => {\n    it('pads prefixes with spaces', async () => {\n        const lines = await run('--pad-prefix -n foo,barbaz \"echo foo\" \"echo bar\"').getLogLines();\n\n        expect(lines).toContainEqual(expect.stringContaining('[foo   ]'));\n        expect(lines).toContainEqual(expect.stringContaining('[barbaz]'));\n    });\n});\n\ndescribe('--restart-tries', () => {\n    it('changes how many times a command will restart', async () => {\n        const lines = await run('--restart-tries 1 \"exit 1\"').getLogLines();\n\n        expect(lines).toEqual([\n            expect.stringContaining('[0] exit 1 exited with code 1'),\n            expect.stringContaining('[0] exit 1 restarted'),\n            expect.stringContaining('[0] exit 1 exited with code 1'),\n        ]);\n    });\n});\n\ndescribe('--kill-others', () => {\n    describe('kills on success', () => {\n        it.each(['--kill-others', '-k'])('%s', async (arg) => {\n            const lines = await run(\n                `${arg} \"node __fixtures__/sleep.js 10\" \"exit 0\"`,\n            ).getLogLines();\n\n            expect(lines).toContainEqual(expect.stringContaining('[1] exit 0 exited with code 0'));\n            expect(lines).toContainEqual(\n                expect.stringContaining('Sending SIGTERM to other processes'),\n            );\n            expect(lines).toContainEqual(\n                expect.stringMatching(\n                    createKillMessage('[0] node __fixtures__/sleep.js 10', 'SIGTERM'),\n                ),\n            );\n        });\n    });\n\n    it('kills on failure', async () => {\n        const lines = await run(\n            '--kill-others \"node __fixtures__/sleep.js 10\" \"exit 1\"',\n        ).getLogLines();\n\n        expect(lines).toContainEqual(expect.stringContaining('[1] exit 1 exited with code 1'));\n        expect(lines).toContainEqual(expect.stringContaining('Sending SIGTERM to other processes'));\n        expect(lines).toContainEqual(\n            expect.stringMatching(\n                createKillMessage('[0] node __fixtures__/sleep.js 10', 'SIGTERM'),\n            ),\n        );\n    });\n});\n\ndescribe('--kill-others-on-fail', () => {\n    it('does not kill on success', async () => {\n        const lines = await run(\n            '--kill-others-on-fail \"node __fixtures__/sleep.js 0.5\" \"exit 0\"',\n        ).getLogLines();\n\n        expect(lines).toContainEqual(expect.stringContaining('[1] exit 0 exited with code 0'));\n        expect(lines).toContainEqual(\n            expect.stringContaining('[0] node __fixtures__/sleep.js 0.5 exited with code 0'),\n        );\n    });\n\n    it('kills on failure', async () => {\n        const lines = await run(\n            '--kill-others-on-fail \"node __fixtures__/sleep.js 10\" \"exit 1\"',\n        ).getLogLines();\n\n        expect(lines).toContainEqual(expect.stringContaining('[1] exit 1 exited with code 1'));\n        expect(lines).toContainEqual(expect.stringContaining('Sending SIGTERM to other processes'));\n        expect(lines).toContainEqual(\n            expect.stringMatching(\n                createKillMessage('[0] node __fixtures__/sleep.js 10', 'SIGTERM'),\n            ),\n        );\n    });\n});\n\ndescribe('--handle-input', () => {\n    describe('forwards input to first process by default', () => {\n        it.each(['--handle-input', '-i'])('%s', async (arg) => {\n            const child = run(`${arg} \"node __fixtures__/read-echo.js\"`);\n            child.log.subscribe((line) => {\n                if (/READING/.test(line)) {\n                    child.stdin.write('stop\\n');\n                }\n            });\n            const lines = await child.getLogLines();\n            const exit = await child.exit;\n\n            expect(exit.code).toBe(0);\n            expect(lines).toContainEqual(expect.stringContaining('[0] stop'));\n            expect(lines).toContainEqual(\n                expect.stringContaining('[0] node __fixtures__/read-echo.js exited with code 0'),\n            );\n        });\n    });\n\n    it('forwards input to process --default-input-target', async () => {\n        const child = run(\n            '-ki --default-input-target 1 \"node __fixtures__/read-echo.js\" \"node __fixtures__/read-echo.js\"',\n        );\n        child.log.subscribe((line) => {\n            if (/\\[1\\] READING/.test(line)) {\n                child.stdin.write('stop\\n');\n            }\n        });\n        const lines = await child.getLogLines();\n        const exit = await child.exit;\n\n        expect(exit.code).toBeGreaterThan(0);\n        expect(lines).toContainEqual(expect.stringContaining('[1] stop'));\n        expect(lines).toContainEqual(\n            expect.stringMatching(\n                createKillMessage('[0] node __fixtures__/read-echo.js', 'SIGTERM'),\n            ),\n        );\n    });\n\n    it('forwards input to specified process', async () => {\n        const child = run('-ki \"node __fixtures__/read-echo.js\" \"node __fixtures__/read-echo.js\"');\n        child.log.subscribe((line) => {\n            if (/\\[1\\] READING/.test(line)) {\n                child.stdin.write('1:stop\\n');\n            }\n        });\n        const lines = await child.getLogLines();\n        const exit = await child.exit;\n\n        expect(exit.code).toBeGreaterThan(0);\n        expect(lines).toContainEqual(expect.stringContaining('[1] stop'));\n        expect(lines).toContainEqual(\n            expect.stringMatching(\n                createKillMessage('[0] node __fixtures__/read-echo.js', 'SIGTERM'),\n            ),\n        );\n    });\n});\n\ndescribe('--teardown', () => {\n    it('runs teardown commands when input commands exit', async () => {\n        const lines = await run('--teardown \"echo bye\" \"echo hey\"').getLogLines();\n        expect(lines).toEqual([\n            expect.stringContaining('[0] hey'),\n            expect.stringContaining('[0] echo hey exited with code 0'),\n            expect.stringContaining('--> Running teardown command \"echo bye\"'),\n            expect.stringContaining('bye'),\n            expect.stringContaining('--> Teardown command \"echo bye\" exited with code 0'),\n        ]);\n    });\n\n    it('runs multiple teardown commands', async () => {\n        const lines = await run(\n            '--teardown \"echo bye\" --teardown \"echo bye2\" \"echo hey\"',\n        ).getLogLines();\n        expect(lines).toContain('bye');\n        expect(lines).toContain('bye2');\n    });\n});\n\ndescribe('--timings', () => {\n    const defaultTimestampFormatRegex = /\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}.\\d{3}/;\n    const tableTopBorderRegex = /^--> ┌[─┬]+┐$/;\n    const tableHeaderRowRegex = /^--> │ name +│ duration +│ exit code +│ killed +│ command +│$/;\n    const tableBottomBorderRegex = /^--> └[─┴]+┘$/;\n\n    const timingsTests = {\n        'shows timings on success': ['node __fixtures__/sleep.js 0.5', 'exit 0'],\n        'shows timings on failure': ['node __fixtures__/sleep.js 0.75', 'exit 1'],\n    };\n    it.each(Object.entries(timingsTests))('%s', async (_, commands) => {\n        const lines = await run(\n            `--timings ${commands.map((command) => `\"${command}\"`).join(' ')}`,\n        ).getLogLines();\n\n        // Expect output to contain process start / stop messages for each command\n        commands.forEach((command, index) => {\n            const escapedCommand = escapeRegExp(command);\n            expect(lines).toContainEqual(\n                expect.stringMatching(\n                    new RegExp(\n                        `^\\\\[${index}] ${escapedCommand} started at ${defaultTimestampFormatRegex.source}$`,\n                    ),\n                ),\n            );\n            expect(lines).toContainEqual(\n                expect.stringMatching(\n                    new RegExp(\n                        `^\\\\[${index}] ${escapedCommand} stopped at ${defaultTimestampFormatRegex.source} after (\\\\d|,)+ms$`,\n                    ),\n                ),\n            );\n        });\n\n        // Expect output to contain timings table\n        expect(lines).toContainEqual(expect.stringMatching(tableTopBorderRegex));\n        expect(lines).toContainEqual(expect.stringMatching(tableHeaderRowRegex));\n        expect(lines).toContainEqual(expect.stringMatching(tableBottomBorderRegex));\n    });\n});\n\ndescribe('--passthrough-arguments', () => {\n    it('argument placeholders are properly replaced when passthrough-arguments is enabled', async () => {\n        const lines = await run('--passthrough-arguments \"echo {1}\" -- echo').getLogLines();\n\n        expect(lines).toContainEqual(expect.stringContaining('[0] echo echo exited with code 0'));\n    });\n\n    it('argument placeholders are not replaced when passthrough-arguments is disabled', async () => {\n        const lines = await run('\"echo {1}\" -- echo').getLogLines();\n\n        expect(lines).toContainEqual(expect.stringContaining('[0] echo {1} exited with code 0'));\n        expect(lines).toContainEqual(expect.stringContaining('[1] echo exited with code 0'));\n    });\n});\n"
  },
  {
    "path": "bin/index.ts",
    "content": "#!/usr/bin/env node\nimport process from 'node:process';\n\nimport yargs from 'yargs';\nimport { hideBin } from 'yargs/helpers';\n\nimport { assertDeprecated } from '../lib/assert.js';\nimport * as defaults from '../lib/defaults.js';\nimport { concurrently } from '../lib/index.js';\nimport { castArray } from '../lib/utils.js';\nimport { readPackageJson } from './read-package-json.js';\n\nconst version = String(readPackageJson().version);\nconst epilogue = `For documentation and more examples, visit:\\nhttps://github.com/open-cli-tools/concurrently/tree/v${version}/docs`;\n\n// Clean-up arguments (yargs expects only the arguments after the program name)\nconst program = yargs(hideBin(process.argv))\n    .parserConfiguration({\n        // Avoids options that can be specified multiple times from requiring a `--` to pass commands\n        'greedy-arrays': false,\n        // Makes sure that --passthrough-arguments works correctly\n        'populate--': true,\n    })\n    .usage('$0 [options] <command ...>')\n    .help('h')\n    .alias('h', 'help')\n    .version(version)\n    .alias('version', 'v')\n    .alias('version', 'V')\n    // TODO: Add some tests for this.\n    .env('CONCURRENTLY')\n    .options({\n        // General\n        'max-processes': {\n            alias: 'm',\n            describe:\n                'How many processes should run at once.\\n' +\n                'New processes only spawn after all restart tries of a process.\\n' +\n                'Exact number or a percent of CPUs available (for example \"50%\")',\n            type: 'string',\n        },\n        names: {\n            alias: 'n',\n            describe:\n                'List of custom names to be used in prefix template.\\n' +\n                'Example names: \"main,browser,server\"',\n            type: 'string',\n        },\n        'name-separator': {\n            describe:\n                'The character to split <names> on. Example usage:\\n' +\n                '-n \"styles|scripts|server\" --name-separator \"|\"',\n            default: defaults.nameSeparator,\n        },\n        success: {\n            alias: 's',\n            describe:\n                'Which command(s) must exit with code 0 in order for concurrently exit with ' +\n                'code 0 too. Options are:\\n' +\n                '- \"first\" for the first command to exit;\\n' +\n                '- \"last\" for the last command to exit;\\n' +\n                '- \"all\" for all commands;\\n' +\n                // Note: not a typo. Multiple commands can have the same name.\n                '- \"command-{name}\"/\"command-{index}\" for the commands with that name or index;\\n' +\n                '- \"!command-{name}\"/\"!command-{index}\" for all commands but the ones with that ' +\n                'name or index.\\n',\n            default: defaults.success,\n        },\n        raw: {\n            alias: 'r',\n            describe:\n                'Output only raw output of processes, disables prettifying ' +\n                'and concurrently coloring.',\n            type: 'boolean',\n        },\n        // This one is provided for free. Chalk reads this itself and removes colors.\n        // https://www.npmjs.com/package/chalk#supportscolor\n        'no-color': {\n            describe: 'Disables colors from logging',\n            type: 'boolean',\n        },\n        hide: {\n            describe:\n                'Comma-separated list of processes to hide the output.\\n' +\n                'The processes can be identified by their name or index.',\n            default: defaults.hide,\n            type: 'string',\n        },\n        group: {\n            alias: 'g',\n            describe: 'Order the output as if the commands were run sequentially.',\n            type: 'boolean',\n        },\n        timings: {\n            describe: 'Show timing information for all processes.',\n            type: 'boolean',\n            default: defaults.timings,\n        },\n        'passthrough-arguments': {\n            alias: 'P',\n            describe:\n                'Passthrough additional arguments to commands (accessible via placeholders) ' +\n                'instead of treating them as commands.',\n            type: 'boolean',\n            default: defaults.passthroughArguments,\n        },\n        teardown: {\n            describe:\n                'Clean up command(s) to execute before exiting concurrently. Might be specified multiple times.\\n' +\n                \"These aren't prefixed and they don't affect concurrently's exit code.\",\n            type: 'string',\n            array: true,\n        },\n\n        // Kill others\n        'kill-others': {\n            alias: 'k',\n            describe: 'Kill other processes once the first exits.',\n            type: 'boolean',\n        },\n        'kill-others-on-fail': {\n            describe: 'Kill other processes if one exits with non zero status code.',\n            type: 'boolean',\n        },\n        'kill-signal': {\n            alias: 'ks',\n            describe:\n                'Signal to send to other processes if one exits or dies. (SIGTERM/SIGKILL, defaults to SIGTERM)',\n            type: 'string',\n            default: defaults.killSignal,\n        },\n        'kill-timeout': {\n            describe: 'How many milliseconds to wait before forcing process terminating.',\n            type: 'number',\n        },\n\n        // Prefix\n        prefix: {\n            alias: 'p',\n            describe:\n                'Prefix used in logging for each process.\\n' +\n                'Possible values: index, pid, time, command, name, none, or a template. ' +\n                'Example template: \"{time}-{pid}\"',\n            defaultDescription: 'index or name (when --names is set)',\n            type: 'string',\n        },\n        'prefix-colors': {\n            alias: 'c',\n            describe:\n                'Comma-separated list of Chalk colors to use on prefixes. ' +\n                'If there are more commands than colors, the last color will be repeated.\\n' +\n                '- Available modifiers: reset, bold, dim, italic, underline, inverse, hidden, strikethrough\\n' +\n                '- Available colors: black, red, green, yellow, blue, magenta, cyan, white, gray, \\n' +\n                'any hex values for colors (e.g. #23de43) or auto for an automatically picked color\\n' +\n                '- Available background colors: bgBlack, bgRed, bgGreen, bgYellow, bgBlue, bgMagenta, bgCyan, bgWhite\\n' +\n                'See https://www.npmjs.com/package/chalk for more information.',\n            default: defaults.prefixColors,\n            type: 'string',\n        },\n        'prefix-length': {\n            alias: 'l',\n            describe:\n                'Limit how many characters of the command is displayed in prefix. ' +\n                'The option can be used to shorten the prefix when it is set to \"command\"',\n            default: defaults.prefixLength,\n            type: 'number',\n        },\n        'pad-prefix': {\n            describe: 'Pads short prefixes with spaces so that the length of all prefixes match',\n            type: 'boolean',\n        },\n        'timestamp-format': {\n            alias: 't',\n            describe:\n                'Specify the timestamp in Unicode format:\\n' +\n                'https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table',\n            default: defaults.timestampFormat,\n            type: 'string',\n        },\n\n        // Restarting\n        'restart-tries': {\n            describe:\n                'How many times a process that died should restart.\\n' +\n                'Negative numbers will make the process restart forever.',\n            default: defaults.restartTries,\n            type: 'number',\n        },\n        'restart-after': {\n            describe: 'Delay before restarting the process, in milliseconds, or \"exponential\".',\n            default: defaults.restartDelay,\n            type: 'string',\n        },\n\n        // Input\n        'handle-input': {\n            alias: 'i',\n            describe:\n                'Whether input should be forwarded to the child processes. ' +\n                'See examples for more information.',\n            type: 'boolean',\n        },\n        'default-input-target': {\n            default: defaults.defaultInputTarget,\n            describe:\n                'Identifier for child process to which input on stdin ' +\n                'should be sent if not specified at start of input.\\n' +\n                'Can be either the index or the name of the process.',\n        },\n    })\n    .group(\n        ['m', 'n', 'name-separator', 's', 'r', 'no-color', 'hide', 'g', 'timings', 'P', 'teardown'],\n        'General',\n    )\n    .group(['p', 'c', 'l', 't', 'pad-prefix'], 'Prefix styling')\n    .group(['i', 'default-input-target'], 'Input handling')\n    .group(['k', 'kill-others-on-fail', 'kill-signal', 'kill-timeout'], 'Killing other processes')\n    .group(['restart-tries', 'restart-after'], 'Restarting')\n    .epilogue(epilogue);\n\nconst args = program.parseSync();\nassertDeprecated(\n    args.nameSeparator === defaults.nameSeparator,\n    'name-separator',\n    'Use commas as name separators instead.',\n);\n\n// Get names of commands by the specified separator\nconst names = (args.names || '').split(args.nameSeparator);\n\nconst additionalArguments = castArray(args['--'] ?? []).map(String);\nconst commands = args.passthroughArguments ? args._ : args._.concat(additionalArguments);\n\nif (!commands.length) {\n    program.showHelp();\n    process.exit();\n}\n\nconcurrently(\n    commands.map((command, index) => ({\n        command: String(command),\n        name: names[index],\n    })),\n    {\n        handleInput: args.handleInput,\n        defaultInputTarget: args.defaultInputTarget,\n        killOthersOn: args.killOthers\n            ? ['success', 'failure']\n            : args.killOthersOnFail\n              ? ['failure']\n              : [],\n        killSignal: args.killSignal,\n        killTimeout: args.killTimeout,\n        maxProcesses: args.maxProcesses,\n        raw: args.raw,\n        hide: args.hide.split(','),\n        group: args.group,\n        prefix: args.prefix,\n        prefixColors: args.prefixColors.split(','),\n        prefixLength: args.prefixLength,\n        padPrefix: args.padPrefix,\n        restartDelay:\n            args.restartAfter === 'exponential' ? 'exponential' : Number(args.restartAfter),\n        restartTries: args.restartTries,\n        successCondition: args.success,\n        timestampFormat: args.timestampFormat,\n        timings: args.timings,\n        teardown: args.teardown,\n        additionalArguments: args.passthroughArguments ? additionalArguments : undefined,\n    },\n).result.then(\n    () => process.exit(0),\n    () => process.exit(1),\n);\n"
  },
  {
    "path": "bin/read-package-json.ts",
    "content": "import { readFileSync } from 'node:fs';\nimport { createRequire } from 'node:module';\n\n/**\n * Read the package.json file of `concurrently`\n */\nexport function readPackageJson(): Record<string, unknown> {\n    let resolver;\n    try {\n        resolver = require.resolve;\n    } catch {\n        resolver = createRequire(import.meta.url).resolve;\n    }\n    const path = resolver('concurrently/package.json');\n    const content = readFileSync(path, 'utf8');\n    return JSON.parse(content);\n}\n"
  },
  {
    "path": "docs/README.md",
    "content": "# Concurrently Documentation\n\n## CLI\n\nThese articles cover using concurrently through CLI:\n\n- [Prefixing](./cli/prefixing.md)\n- [Output Control](./cli/output-control.md)\n- [Success Conditions](./cli/success.md)\n- [Shortcuts](./cli/shortcuts.md)\n- [Terminating Commands](./cli/terminating.md)\n- [Restarting Commands](./cli/restarting.md)\n- [Input Handling](./cli/input-handling.md)\n- [Passthrough Arguments](./cli/passthrough-arguments.md)\n- [Configuration](./cli/configuration.md)\n"
  },
  {
    "path": "docs/cli/configuration.md",
    "content": "# Configuration\n\nYou might want to configure concurrently to always have certain flags on.\nAny of concurrently's flags can be set via environment variables that are prefixed with `CONCURRENTLY_`.\n\n```bash\n$ export CONCURRENTLY_KILL_OTHERS=true\n$ export CONCURRENTLY_HANDLE_INPUT=true\n# Equivalent to passing --kill-others and --handle-input\n$ concurrently nodemon \"echo 'hey nodemon, you won't last long'\"\n```\n"
  },
  {
    "path": "docs/cli/input-handling.md",
    "content": "# Input Handling\n\nBy default, concurrently doesn't send input to any commands it spawns.<br/>\nIn the below example, typing `rs` to manually restart [nodemon](https://nodemon.io/) does nothing:\n\n```bash\n$ concurrently 'nodemon' 'npm run watch-js'\nrs\n```\n\nTo turn on input handling, it's necessary to set the `--handle-input`/`-i` flag.<br/>\nThis will send `rs` to the first command:\n\n```bash\n$ concurrently --handle-input 'nodemon' 'npm run watch-js'\nrs\n```\n\nTo send input to a different command instead, it's possible to prefix the input with the command index, followed by a `:`.<br/>\nFor example, the below sends `rs` to the second command:\n\n```bash\n$ concurrently --handle-input 'npm run watch-js' 'nodemon'\n1:rs\n```\n\nIf the command has a name, it's also possible to target it using that command's name:\n\n```bash\n$ concurrently --handle-input --names js,server 'npm run watch-js' 'nodemon'\nserver:rs\n```\n\nIt's also possible to change the default command that receives input.<br/>\nTo do this, set the `--default-input-target` flag to a command's index or name.\n\n```bash\n$ concurrently --handle-input --default-input-target 1 'npm run watch-js' 'nodemon'\nrs\n```\n"
  },
  {
    "path": "docs/cli/output-control.md",
    "content": "# Output Control\n\nconcurrently offers a few ways to control a command's output.\n\n## Hiding\n\nA command's outputs (and all its events) can be hidden by using the `--hide` flag.\n\n```bash\n$ concurrently --hide 0 'echo Hello there' 'echo General Kenobi!'\n[1] General Kenobi!\n[1] echo 'General Kenobi!' exited with code 0\n```\n\n## Grouping\n\nIt might be useful at times to make sure that the commands outputs are grouped together, while running them in parallel.<br/>\nThis can be done with the `--group` flag.\n\n```bash\n$ concurrently --group 'echo Hello there && sleep 2 && echo General Kenobi!' 'echo hi Star Wars fans'\n[0] Hello there\n[0] General Kenobi!\n[0] echo Hello there && sleep 2 && echo 'General Kenobi!' exited with code 0\n[1] hi Star Wars fans\n[1] echo hi Star Wars fans exited with code 0\n```\n\n## No Colors\n\nWhen 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.\n\n```bash\n$ concurrently -c red,blue --no-color 'echo Hello there' 'echo General Kenobi!'\n```\n"
  },
  {
    "path": "docs/cli/passthrough-arguments.md",
    "content": "# Passthrough Arguments\n\nIf you have a shortcut for running a specific combination of commands through concurrently,\nyou might need at some point to pass additional arguments/flags to some of these.\n\nFor example, imagine you have in your `package.json` file scripts like this:\n\n```jsonc\n{\n  // ...\n  \"scripts\": {\n    \"build:client\": \"tsc -p client\",\n    \"build:server\": \"tsc -p server\",\n    \"build\": \"concurrently npm:build:client npm:build:server\",\n  },\n}\n```\n\nIf you wanted to run only either `build:server` or `build:client` with an additional `--noEmit` flag,\nyou can do so with `npm run build:server -- --noEmit`, for example.<br/>\nHowever, if you want to do that while using concurrently, as `npm run build -- --noEmit` for example,\nyou might find that concurrently actually parses `--noEmit` as its own flag, which does nothing,\nbecause it doesn't exist.\n\nTo solve this, you can set the `--passthrough-arguments`/`-P` flag, which instructs concurrently to\ntake everything after a `--` as additional arguments that are passed through to the input commands\nvia a few placeholder styles:\n\n## Single argument\n\nWe can modify the original `build` script to pass a single additional argument/flag to a script by using\na 1-indexed `{number}` placeholder to the command you want it to apply to:\n\n```jsonc\n{\n  // ...\n  \"scripts\": {\n    // ...\n    \"build\": \"concurrently -P 'npm:build:client -- {1}' npm:build:server --\",\n    \"typecheck\": \"npm run build -- --noEmit\",\n  },\n}\n```\n\nWith this, running `npm run typecheck` will pass `--noEmit` only to `npm run build:client`.\n\n## All arguments\n\nIn the original `build` example script, you're more likely to want to pass every additional argument/flag\nto your commands. This can be done with the `{@}` placeholder.\n\n```jsonc\n{\n  // ...\n  \"scripts\": {\n    // ...\n    \"build\": \"concurrently -P 'npm:build:client -- {@}' 'npm:build:server -- {@}' --\",\n    \"typecheck\": \"npm run build -- --watch --noEmit\",\n  },\n}\n```\n\nIn the above example, both `--watch` and `--noEmit` are passed to each command.\n\n## All arguments, combined\n\nIf for some reason you wish to combine all additional arguments into a single one, you can do that with the `{*}` placeholder,\nwhich wraps the arguments in quotes.\n\n```jsonc\n{\n  // ...\n  \"scripts\": {\n    // ...\n    \"build\": \"concurrently -P 'npm:build:client -- --outDir {*}/client' 'npm:build:server -- --outDir {*}/server' -- $(date)\",\n  },\n}\n```\n\nIn 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.\n"
  },
  {
    "path": "docs/cli/prefixing.md",
    "content": "# Prefixing\n\n## Prefix Styles\n\nconcurrently will by default prefix each command's outputs with a zero-based index, wrapped in square brackets:\n\n```bash\n$ concurrently 'echo Hello there' \"echo 'General Kenobi!'\"\n[0] Hello there\n[1] General Kenobi!\n[0] echo Hello there exited with code 0\n[1] echo 'General Kenobi!' exited with code 0\n```\n\nIf you've given the commands names, they are used instead:\n\n```bash\n$ concurrently --names one,two 'echo Hello there' \"echo 'General Kenobi!'\"\n[one] Hello there\n[two] General Kenobi!\n[one] echo Hello there exited with code 0\n[two] echo 'General Kenobi!' exited with code 0\n```\n\nThere are other prefix styles available too:\n\n| Style     | Description                       |\n| --------- | --------------------------------- |\n| `index`   | Zero-based command's index        |\n| `name`    | The command's name                |\n| `command` | The command's line                |\n| `time`    | Time of output                    |\n| `pid`     | ID of the command's process (PID) |\n| `none`    | No prefix                         |\n\nAny of these can be used by setting the `--prefix`/`-p` flag. For example:\n\n```bash\n$ concurrently --prefix pid 'echo Hello there' 'echo General Kenobi!'\n[2222] Hello there\n[2223] General Kenobi!\n[2222] echo Hello there exited with code 0\n[2223] echo 'General Kenobi!' exited with code 0\n```\n\nIt's also possible to have a prefix based on a template. Any of the styles listed above can be used by wrapping it in `{}`.\nDoing so will also remove the square brackets:\n\n```bash\n$ concurrently --prefix '{index}-{pid}' 'echo Hello there' 'echo General Kenobi!'\n0-2222 Hello there\n1-2223 General Kenobi!\n0-2222 echo Hello there exited with code 0\n1-2223 echo 'General Kenobi!' exited with code 0\n```\n\n## Prefix Colors\n\nBy default, there are no colors applied to concurrently prefixes, and they just use whatever the terminal's defaults are.\n\nThis can be changed by using the `--prefix-colors`/`-c` flag, which takes a comma-separated list of colors to use.<br/>\nThe available values are color names (e.g. `green`, `magenta`, `gray`, etc), a hex value (such as `#23de43`), or `auto`, to automatically select a color.\n\n```bash\n$ concurrently -c red,blue 'echo Hello there' 'echo General Kenobi!'\n```\n\n<details>\n<summary>List of available color names</summary>\n\n- `black`\n- `blue`\n- `cyan`\n- `green`\n- `gray`\n- `magenta`\n- `red`\n- `white`\n- `yellow`\n</details>\n\nColors can take modifiers too. Several can be applied at once by appending `.<modifier 1>.<modifier 2>` and so on.\n\n```bash\n$ concurrently -c '#23de43.inverse,bold.blue.dim' 'echo Hello there' 'echo General Kenobi!'\n```\n\n<details>\n<summary>List of available modifiers</summary>\n\n- `reset`\n- `bold`\n- `dim`\n- `hidden`\n- `inverse`\n- `italic`\n- `strikethrough`\n- `underline`\n</details>\n\nA background color can be set in a similarly fashion.\n\n```bash\n$ concurrently -c bgGray,red.bgBlack 'echo Hello there' 'echo General Kenobi!'\n```\n\n<details>\n<summary>List of available background color names</summary>\n\n- `bgBlack`\n- `bgBlue`\n- `bgCyan`\n- `bgGreen`\n- `bgGray`\n- `bgMagenta`\n- `bgRed`\n- `bgWhite`\n- `bgYellow`\n</details>\n\n## Prefix Length\n\nWhen using the `command` prefix style, it's possible that it'll be too long.<br/>\nIt can be limited by setting the `--prefix-length`/`-l` flag:\n\n```bash\n$ concurrently -p command -l 10 'echo Hello there' 'echo General Kenobi!'\n[echo..here] Hello there\n[echo..bi!'] General Kenobi!\n[echo..here] echo Hello there exited with code 0\n[echo..bi!'] echo 'General Kenobi!' exited with code 0\n```\n\nIt's also possible that some prefixes are too short, and you want all of them to have the same length.<br/>\nThis can be done by setting the `--pad-prefix` flag:\n\n```bash\n$ concurrently -n foo,barbaz --pad-prefix 'echo Hello there' 'echo General Kenobi!'\n[foo   ] Hello there\n[foo   ] echo Hello there exited with code 0\n[barbaz] General Kenobi!\n[barbaz] echo 'General Kenobi!' exited with code 0\n```\n\n> [!NOTE]\n> 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.<br/>\n> This might happen, for example, if the PID was 99 and it's now 100.\n"
  },
  {
    "path": "docs/cli/restarting.md",
    "content": "# Restarting Commands\n\nSometimes it's useful to have commands that exited with a non-zero status to restart automatically.<br/>\nconcurrently lets you configure how many times you wish for such a command to restart through the `--restart-tries` flag:\n\n```bash\n$ concurrently --restart-tries 2 'exit 1'\n[0] exit 1 exited with code 1\n[0] exit 1 restarted\n[0] exit 1 exited with code 1\n[0] exit 1 restarted\n[0] exit 1 exited with code 1\n```\n\nSometimes, it might be interesting to have commands wait before restarting.<br/>\nTo do this, simply set `--restart-after` to a the number of milliseconds you'd like to delay restarting.\n\n```bash\n$ concurrently -p time --restart-tries 1 --restart-after 3000 'exit 1'\n[2024-09-01 23:43:55.871] exit 1 exited with code 1\n[2024-09-01 23:43:58.874] exit 1 restarted\n[2024-09-01 23:43:58.891] exit 1 exited with code 1\n```\n\nIf a command is not having success spawning, you might want to instead apply an exponential back-off.<br/>\nSet `--restart-after exponential` to have commands respawn with a `2^N` seconds delay.\n\n```bash\n$ concurrently -p time --restart-tries 3 --restart-after exponential 'exit 1'\n\n[2024-09-01 23:49:01.124] exit 1 exited with code 1\n[2024-09-01 23:49:02.127] exit 1 restarted\n[2024-09-01 23:49:02.139] exit 1 exited with code 1\n[2024-09-01 23:49:04.141] exit 1 restarted\n[2024-09-01 23:49:04.157] exit 1 exited with code 1\n[2024-09-01 23:49:08.158] exit 1 restarted\n[2024-09-01 23:49:08.174] exit 1 exited with code 1\n```\n"
  },
  {
    "path": "docs/cli/shortcuts.md",
    "content": "# Command Shortcuts\n\nPackage managers that execute scripts from a `package.json` or `deno.(json|jsonc)` file can be shortened when in concurrently.<br/>\nThe following are supported:\n\n| Syntax          | Expands to            |\n| --------------- | --------------------- |\n| `npm:<script>`  | `npm run <script>`    |\n| `pnpm:<script>` | `pnpm run <script>`   |\n| `yarn:<script>` | `yarn run <script>`   |\n| `bun:<script>`  | `bun run <script>`    |\n| `node:<script>` | `node --run <script>` |\n| `deno:<script>` | `deno task <script>`  |\n\n> [!NOTE]\n>\n> `node --run` is only available from [Node 22 onwards](https://nodejs.org/en/blog/announcements/v22-release-announce#running-packagejson-scripts).\n\nFor example, given the following `package.json` contents:\n\n```jsonc\n{\n  // ...\n  \"scripts\": {\n    \"lint:js\": \"...\",\n    \"lint:ts\": \"...\",\n    \"lint:fix:js\": \"...\",\n    \"lint:fix:ts\": \"...\",\n    // ...\n  },\n  // ...\n}\n```\n\nIt's possible to run some of these with the following command line:\n\n```bash\n$ concurrently 'pnpm:lint:js'\n# Is equivalent to\n$ concurrently -n lint:js 'pnpm run lint:js'\n```\n\nNote that the command automatically receives a name equal to the script name.\n\nIf you have several scripts with similar name patterns, you can use the `*` wildcard to run all of them at once.<br/>\nThe spawned commands will receive names set to whatever the `*` wildcard matched.\n\n```bash\n$ concurrently 'npm:lint:fix:*'\n# is equivalent to\n$ concurrently -n js,ts 'npm run lint:fix:js' 'npm run lint:fix:ts'\n```\n\nIf you specify a command name when using wildcards, it'll be a prefix of what the `*` wildcard matched:\n\n```bash\n$ concurrently -n fix: 'npm:lint:fix:*'\n# is equivalent to\n$ concurrently -n fix:js,fix:ts 'npm run lint:fix:js' 'npm run lint:fix:ts'\n```\n\nFiltering out commands matched by wildcard is also possible. Do this with by including `(!<some pattern>)` in the command line:\n\n```bash\n$ concurrently 'yarn:lint:*(!fix)'\n# is equivalent to\n$ concurrently -n js,ts 'yarn run lint:js' 'yarn run lint:ts'\n```\n\n> [!NOTE]\n> If you use this syntax with double quotes (`\"`), bash and other shells might fail\n> parsing it. You'll need to escape the `!`, or use single quote (`'`) instead.<br/>\n> See [here](https://serverfault.com/a/208266/160539) for more information.\n"
  },
  {
    "path": "docs/cli/success.md",
    "content": "# Success Conditions\n\nWhen you're using concurrently in shell scripts or CI pipelines, the exit code matters.  \nIt determines whether the next step runs, or if the script stops with a failure.\n\nYou can control concurrently's exit code using the `--success` flag.  \nThis tells it **which command(s)** must succeed (exit with code `0`) for concurrently to return success overall.\n\nThere are several possible values:\n\n## `all`\n\nAll commands must exit with code `0`.\nThis is the default value.\n\n## `first`\n\nThe first command to exit must do so with code `0`.\n\n```bash\n# ✅ Exits with code 0 — second command exits first and succeeds\n$ concurrently --success first 'sleep 1 && exit 1' 'exit 0'\n\n# ❌ Exits with a non-zero code — second command exits first, but with code 1\n$ concurrently --success first 'sleep 1 && exit 0' 'exit 1'\n```\n\n## `last`\n\nThe last command to exit must do so with code `0`.\n\n```bash\n# ✅ Exits with code 0 - first command exits last and succeeds\n$ concurrently --success last 'sleep 1 && exit 0' 'exit 1'\n\n# ❌ Exits with a non-zero code — first command exits last, but with code 1\n$ concurrently --success last 'sleep 1 && exit 1' 'exit 0'\n```\n\n## `command-{name}` or `command-{index}`\n\nA specific command, by name or index, must exit with code `0`.\n\n```bash\n# Exits with code 0 only if 'npm test' (index 1) passes.\n$ concurrently --success command-1 --kill-others 'npm run server' 'npm test'\n\n# Exits with code 0 only if 'test' command passes.\n$ concurrently --success command-test --names server,test --kill-others \\\n    'npm start' \\\n    'npm test'\n```\n\n> [!TIP]\n> Use `--kill-others` to kill a long-running process, such as a server, once tests pass.\n\n## `!command-{name}` or `!command-{index}`\n\nAll but a specific command, by name or index, must exit with code `0`.\n\n```bash\n# Ignores 'npm start'; all others must succeed\n$ concurrently --success '!command-2' --kill-others \\\n    'npm test' \\\n    'npm build' \\\n    'npm start'\n\n# Ignores 'server'; all others must succeed\n$ concurrently --success '!command-server' --names test,build,server --kill-others \\\n    'npm test' \\\n    'npm build' \\\n    'npm start'\n```\n"
  },
  {
    "path": "docs/cli/terminating.md",
    "content": "# Terminating Commands\n\nIt's possible to have concurrently terminate other commands when one of them exits.<br/>\nThis can be done in the following ways:\n\n## Terminating on either success or error\n\nBy using the `--kill-others` flag, concurrently will terminate other commands once the first one exits,\nno matter the exit code.<br/>\nThis is useful to terminate the server process once the test is done.\n\n```bash\n$ concurrently --kill-others --names server,test 'npm start' 'npm test'\n```\n\n## Terminating on error only\n\nBy using the `--kill-others-on-fail` flag, concurrently will terminate other commands any command\nexits with a non-zero code.<br/>\nThis is useful if you're building multiple applications, and you want to abort the others once you know\nthat any of them is broken.\n\n```bash\n$ concurrently --kill-others-on-fail 'npm run app1:build' 'npm run app2:build'\n```\n\n## Configuring termination\n\n### Kill Signal\n\nIt's possible to configure which signal you want to send when terminating commands with the `--kill-signal` flag.\nThe default is `SIGTERM`, but it's also possible to send `SIGKILL`.\n\n```bash\n$ concurrently --kill-others --kill-signal SIGKILL 'npm start' 'npm test'\n```\n\n### Timeout\n\nIn case you have a misbehaving process that ignores the kill signal, you can force kill it after some\ntimeout (in milliseconds) by using the `--kill-timeout` flag.\nThis sends a `SIGKILL`, which cannot be caught.\n\n```bash\n$ concurrently --kill-others --kill-timeout 1000 'sleep 1 && echo bye' './misbehaving'\n[0] bye\n[0] sleep 1 && echo bye exited with code 0\n--> Sending SIGTERM to other processes..\n[1] IGNORING SIGNAL\n--> Sending SIGKILL to 1 processes..\n[1] ./misbehaving exited with code SIGKILL\n```\n"
  },
  {
    "path": "eslint.config.js",
    "content": "// @ts-check\n\nimport eslint from '@eslint/js';\nimport pluginVitest from '@vitest/eslint-plugin';\nimport { defineConfig } from 'eslint/config';\nimport gitignore from 'eslint-config-flat-gitignore';\nimport pluginImportLite from 'eslint-plugin-import-lite';\nimport pluginPrettierRecommended from 'eslint-plugin-prettier/recommended';\nimport pluginSimpleImportSort from 'eslint-plugin-simple-import-sort';\nimport globals from 'globals';\nimport tseslint from 'typescript-eslint';\n\nexport default defineConfig(\n    gitignore(),\n    {\n        languageOptions: {\n            globals: {\n                ...globals.node,\n            },\n            ecmaVersion: 2023,\n        },\n    },\n    eslint.configs.recommended,\n    tseslint.configs.recommended,\n    {\n        rules: {\n            curly: 'error',\n            eqeqeq: ['error', 'always', { null: 'ignore' }],\n            'no-var': 'error',\n            'no-console': 'error',\n            'prefer-const': 'error',\n            'prefer-object-spread': 'error',\n            '@typescript-eslint/no-unused-vars': [\n                'error',\n                {\n                    varsIgnorePattern: '^_',\n                },\n            ],\n        },\n    },\n    { files: ['**/__fixtures__/**/*.{js,ts}'], rules: { 'no-console': 'off' } },\n    {\n        plugins: {\n            'simple-import-sort': pluginSimpleImportSort,\n            import: pluginImportLite,\n        },\n        rules: {\n            'simple-import-sort/imports': 'error',\n            'simple-import-sort/exports': 'error',\n            'import/first': 'error',\n            'import/newline-after-import': 'error',\n            'import/no-duplicates': 'error',\n        },\n    },\n    {\n        files: ['**/*.spec.ts'],\n        plugins: {\n            vitest: pluginVitest,\n        },\n        rules: {\n            ...pluginVitest.configs.recommended.rules,\n            // Currently produces false positives, see https://github.com/vitest-dev/eslint-plugin-vitest/issues/775\n            'vitest/prefer-called-exactly-once-with': 'off',\n        },\n    },\n    pluginPrettierRecommended,\n);\n"
  },
  {
    "path": "lib/__fixtures__/create-mock-instance.ts",
    "content": "import { MockedObject, vi } from 'vitest';\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function createMockInstance<T>(constructor: new (...args: any[]) => T): MockedObject<T> {\n    return new (vi.mockObject(constructor))() as MockedObject<T>;\n}\n"
  },
  {
    "path": "lib/__fixtures__/fake-command.ts",
    "content": "import EventEmitter from 'node:events';\nimport { PassThrough, Writable } from 'node:stream';\n\nimport { vi } from 'vitest';\n\nimport { ChildProcess, CloseEvent, Command, CommandInfo } from '../command.js';\nimport { createMockInstance } from './create-mock-instance.js';\n\nexport class FakeCommand extends Command {\n    constructor(name = 'foo', command = 'echo foo', index = 0, info?: Partial<CommandInfo>) {\n        super(\n            {\n                index,\n                name,\n                command,\n                ...info,\n            },\n            {},\n            vi.fn(),\n            vi.fn(),\n        );\n\n        this.stdin = createMockInstance(Writable);\n        this.start = vi.fn();\n        this.kill = vi.fn();\n    }\n}\n\nexport const createFakeProcess = (pid: number): ChildProcess =>\n    Object.assign(new EventEmitter(), {\n        pid,\n        send: vi.fn(),\n        stdin: new PassThrough(),\n        stdout: new PassThrough(),\n        stderr: new PassThrough(),\n    });\n\nexport const createFakeCloseEvent = (overrides?: Partial<CloseEvent>): CloseEvent => ({\n    command: new FakeCommand(),\n    index: 0,\n    killed: false,\n    exitCode: 0,\n    timings: {\n        startDate: new Date(),\n        endDate: new Date(),\n        durationSeconds: 0,\n    },\n    ...overrides,\n});\n"
  },
  {
    "path": "lib/assert.spec.ts",
    "content": "import { afterEach, describe, expect, it, vi } from 'vitest';\n\nimport { assertDeprecated } from './assert.js';\n\ndescribe('#assertDeprecated()', () => {\n    const consoleMock = vi.spyOn(console, 'warn').mockImplementation(() => {});\n\n    afterEach(() => {\n        vi.clearAllMocks();\n    });\n\n    it('prints warning with name and message when condition is false', () => {\n        assertDeprecated(false, 'example-flag', 'This is an example message.');\n\n        expect(consoleMock).toHaveBeenLastCalledWith(\n            '[concurrently] example-flag is deprecated. This is an example message.',\n        );\n    });\n\n    it('prints same warning only once', () => {\n        assertDeprecated(false, 'example-flag', 'This is an example message.');\n        assertDeprecated(false, 'different-flag', 'This is another message.');\n\n        expect(consoleMock).toBeCalledTimes(1);\n        expect(consoleMock).toHaveBeenLastCalledWith(\n            '[concurrently] different-flag is deprecated. This is another message.',\n        );\n    });\n\n    it('prints nothing if condition is true', () => {\n        assertDeprecated(true, 'example-flag', 'This is an example message.');\n\n        expect(consoleMock).not.toHaveBeenCalled();\n    });\n});\n"
  },
  {
    "path": "lib/assert.ts",
    "content": "const deprecations = new Set<string>();\n\n/**\n * Asserts that some condition is true, and if not, prints a warning about it being deprecated.\n * The message is printed only once.\n */\nexport function assertDeprecated(check: boolean, name: string, message: string) {\n    if (!check && !deprecations.has(name)) {\n        // eslint-disable-next-line no-console\n        console.warn(`[concurrently] ${name} is deprecated. ${message}`);\n        deprecations.add(name);\n    }\n}\n"
  },
  {
    "path": "lib/command-parser/command-parser.d.ts",
    "content": "import { CommandInfo } from '../command.js';\n\n/**\n * A command parser encapsulates a specific logic for mapping `CommandInfo` objects\n * into another `CommandInfo`.\n *\n * A prime example is turning an abstract `npm:foo` into `npm run foo`, but it could also turn\n * the prefix color of a command brighter, or maybe even prefixing each command with `time(1)`.\n */\nexport interface CommandParser {\n    /**\n     * Parses `commandInfo` and returns one or more `CommandInfo`s.\n     *\n     * Returning multiple `CommandInfo` is used when there are multiple possibilities of commands to\n     * run given the original input.\n     * An example of this is when the command contains a wildcard and it must be expanded into all\n     * viable options so that the consumer can decide which ones to run.\n     */\n    parse: (commandInfo: CommandInfo) => CommandInfo | CommandInfo[];\n}\n"
  },
  {
    "path": "lib/command-parser/expand-arguments.spec.ts",
    "content": "import { expect, it } from 'vitest';\n\nimport { CommandInfo } from '../command.js';\nimport { ExpandArguments } from './expand-arguments.js';\n\nconst createCommandInfo = (command: string): CommandInfo => ({\n    command,\n    name: '',\n});\n\nit('returns command as is when no placeholders', () => {\n    const parser = new ExpandArguments(['foo', 'bar']);\n    const commandInfo = createCommandInfo('echo foo');\n    expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo foo' });\n});\n\nit('single argument placeholder is replaced', () => {\n    const parser = new ExpandArguments(['foo', 'bar']);\n    const commandInfo = createCommandInfo('echo {1}');\n    expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo foo' });\n});\n\nit('argument placeholder is replaced and quoted properly', () => {\n    const parser = new ExpandArguments(['foo bar']);\n    const commandInfo = createCommandInfo('echo {1}');\n    expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: \"echo 'foo bar'\" });\n});\n\nit('multiple single argument placeholders are replaced', () => {\n    const parser = new ExpandArguments(['foo', 'bar']);\n    const commandInfo = createCommandInfo('echo {2} {1}');\n    expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo bar foo' });\n});\n\nit('empty replacement with single placeholder and not enough passthrough arguments', () => {\n    const parser = new ExpandArguments(['foo', 'bar']);\n    const commandInfo = createCommandInfo('echo {3}');\n    expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo ' });\n});\n\nit('empty replacement with all placeholder and no passthrough arguments', () => {\n    const parser = new ExpandArguments([]);\n    const commandInfo = createCommandInfo('echo {@}');\n    expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo ' });\n});\n\nit('empty replacement with combined placeholder and no passthrough arguments', () => {\n    const parser = new ExpandArguments([]);\n    const commandInfo = createCommandInfo('echo {*}');\n    expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo ' });\n});\n\nit('all arguments placeholder is replaced', () => {\n    const parser = new ExpandArguments(['foo', 'bar']);\n    const commandInfo = createCommandInfo('echo {@}');\n    expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo foo bar' });\n});\n\nit('combined arguments placeholder is replaced', () => {\n    const parser = new ExpandArguments(['foo', 'bar']);\n    const commandInfo = createCommandInfo('echo {*}');\n    expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: \"echo 'foo bar'\" });\n});\n\nit('escaped argument placeholders are not replaced', () => {\n    const parser = new ExpandArguments(['foo', 'bar']);\n    // Equals to single backslash on command line\n    const commandInfo = createCommandInfo('echo \\\\{1} \\\\{@} \\\\{*}');\n    expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo {1} {@} {*}' });\n});\n"
  },
  {
    "path": "lib/command-parser/expand-arguments.ts",
    "content": "import { quote } from 'shell-quote';\n\nimport { CommandInfo } from '../command.js';\nimport { CommandParser } from './command-parser.js';\n\n/**\n * Replace placeholders with additional arguments.\n */\nexport class ExpandArguments implements CommandParser {\n    constructor(private readonly additionalArguments: string[]) {}\n\n    parse(commandInfo: CommandInfo) {\n        const command = commandInfo.command.replace(\n            /\\\\?\\{([@*]|[1-9]\\d*)\\}/g,\n            (match, placeholderTarget: string) => {\n                // Don't replace the placeholder if it is escaped by a backslash.\n                if (match.startsWith('\\\\')) {\n                    return match.slice(1);\n                }\n\n                if (this.additionalArguments.length > 0) {\n                    // Replace numeric placeholder if value exists in additional arguments.\n                    if (+placeholderTarget <= this.additionalArguments.length) {\n                        return quote([this.additionalArguments[+placeholderTarget - 1]]);\n                    }\n\n                    // Replace all arguments placeholder.\n                    if (placeholderTarget === '@') {\n                        return quote(this.additionalArguments);\n                    }\n\n                    // Replace combined arguments placeholder.\n                    if (placeholderTarget === '*') {\n                        return quote([this.additionalArguments.join(' ')]);\n                    }\n                }\n\n                // Replace placeholder with empty string\n                // if value doesn't exist in additional arguments.\n                return '';\n            },\n        );\n\n        return { ...commandInfo, command };\n    }\n}\n"
  },
  {
    "path": "lib/command-parser/expand-shortcut.spec.ts",
    "content": "import { describe, expect, it } from 'vitest';\n\nimport { CommandInfo } from '../command.js';\nimport { ExpandShortcut } from './expand-shortcut.js';\n\nconst parser = new ExpandShortcut();\n\nconst createCommandInfo = (command: string, name = ''): CommandInfo => ({\n    name,\n    command,\n});\n\nit('returns same command if no prefix is present', () => {\n    const commandInfo = createCommandInfo('echo foo');\n    expect(parser.parse(commandInfo)).toBe(commandInfo);\n});\n\ndescribe.each([\n    ['npm', 'npm run'],\n    ['yarn', 'yarn run'],\n    ['pnpm', 'pnpm run'],\n    ['bun', 'bun run'],\n    ['node', 'node --run'],\n    ['deno', 'deno task'],\n])(`with '%s:' prefix`, (prefix, command) => {\n    it(`expands to \"${command} <script> <args>\"`, () => {\n        const commandInfo = createCommandInfo(`${prefix}:foo -- bar`, 'echo');\n        expect(parser.parse(commandInfo)).toEqual({\n            ...commandInfo,\n            name: 'echo',\n            command: `${command} foo -- bar`,\n        });\n    });\n\n    it('sets name to script name if none', () => {\n        const commandInfo = createCommandInfo(`${prefix}:foo -- bar`);\n        expect(parser.parse(commandInfo)).toEqual({\n            ...commandInfo,\n            name: 'foo',\n            command: `${command} foo -- bar`,\n        });\n    });\n});\n"
  },
  {
    "path": "lib/command-parser/expand-shortcut.ts",
    "content": "import { CommandInfo } from '../command.js';\nimport { CommandParser } from './command-parser.js';\n\n/**\n * Expands shortcuts according to the following table:\n *\n * | Syntax          | Expands to            |\n * | --------------- | --------------------- |\n * | `npm:<script>`  | `npm run <script>`    |\n * | `pnpm:<script>` | `pnpm run <script>`   |\n * | `yarn:<script>` | `yarn run <script>`   |\n * | `bun:<script>`  | `bun run <script>`    |\n * | `node:<script>` | `node --run <script>` |\n * | `deno:<script>` | `deno task <script>`  |\n */\nexport class ExpandShortcut implements CommandParser {\n    parse(commandInfo: CommandInfo) {\n        const [, prefix, script, args] =\n            /^(npm|yarn|pnpm|bun|node|deno):(\\S+)(.*)/.exec(commandInfo.command) || [];\n        if (!script) {\n            return commandInfo;\n        }\n\n        let command: string;\n        if (prefix === 'node') {\n            command = 'node --run';\n        } else if (prefix === 'deno') {\n            command = 'deno task';\n        } else {\n            command = `${prefix} run`;\n        }\n\n        return {\n            ...commandInfo,\n            name: commandInfo.name || script,\n            command: `${command} ${script}${args}`,\n        };\n    }\n}\n"
  },
  {
    "path": "lib/command-parser/expand-wildcard.spec.ts",
    "content": "import fs, { PathOrFileDescriptor } from 'node:fs';\n\nimport { afterEach, beforeEach, describe, expect, it, Mock, vi } from 'vitest';\n\nimport { CommandInfo } from '../command.js';\nimport { ExpandWildcard } from './expand-wildcard.js';\n\nlet parser: ExpandWildcard;\nlet readPackage: Mock;\nlet readDeno: Mock;\n\nconst createCommandInfo = (command: string): CommandInfo => ({\n    command,\n    name: '',\n});\n\nbeforeEach(() => {\n    readDeno = vi.fn();\n    readPackage = vi.fn();\n    parser = new ExpandWildcard(readDeno, readPackage);\n});\n\nafterEach(() => {\n    vi.restoreAllMocks();\n});\n\ndescribe('#readDeno()', () => {\n    it('can read deno.json', () => {\n        const expectedDeno = {\n            name: 'deno',\n            version: '1.14.0',\n        };\n        vi.spyOn(fs, 'existsSync').mockImplementation((path: PathOrFileDescriptor) => {\n            return path === 'deno.json';\n        });\n        vi.spyOn(fs, 'readFileSync').mockImplementation((path: PathOrFileDescriptor) => {\n            if (path === 'deno.json') {\n                return JSON.stringify(expectedDeno);\n            }\n            return '';\n        });\n\n        const actualReadDeno = ExpandWildcard.readDeno();\n        expect(actualReadDeno).toEqual(expectedDeno);\n    });\n\n    it('can read deno.jsonc', () => {\n        const expectedDeno = {\n            name: 'deno',\n            version: '1.14.0',\n        };\n        vi.spyOn(fs, 'existsSync').mockImplementation((path: PathOrFileDescriptor) => {\n            return path === 'deno.jsonc';\n        });\n        vi.spyOn(fs, 'readFileSync').mockImplementation((path: PathOrFileDescriptor) => {\n            if (path === 'deno.jsonc') {\n                return `/* comment */\\n${JSON.stringify(expectedDeno)}`;\n            }\n            return '';\n        });\n\n        const actualReadDeno = ExpandWildcard.readDeno();\n        expect(actualReadDeno).toEqual(expectedDeno);\n    });\n\n    it('prefers deno.json over deno.jsonc', () => {\n        const expectedDeno = {\n            name: 'deno',\n            version: '1.14.0',\n        };\n        vi.spyOn(fs, 'existsSync').mockImplementation((path: PathOrFileDescriptor) => {\n            return path === 'deno.json' || path === 'deno.jsonc';\n        });\n        vi.spyOn(fs, 'readFileSync').mockImplementation((path: PathOrFileDescriptor) => {\n            if (path === 'deno.json') {\n                return JSON.stringify(expectedDeno);\n            }\n            return '';\n        });\n\n        const actualReadDeno = ExpandWildcard.readDeno();\n        expect(actualReadDeno).toEqual(expectedDeno);\n    });\n\n    it('can handle errors reading deno', () => {\n        vi.spyOn(fs, 'existsSync').mockReturnValue(true);\n        vi.spyOn(fs, 'readFileSync').mockImplementation(() => {\n            throw new Error('Error reading deno');\n        });\n\n        expect(() => ExpandWildcard.readDeno()).not.toThrow();\n        expect(ExpandWildcard.readDeno()).toEqual({});\n    });\n});\n\ndescribe('#readPackage()', () => {\n    it('can read package', () => {\n        const expectedPackage = {\n            name: 'concurrently',\n            version: '6.4.0',\n        };\n        vi.spyOn(fs, 'readFileSync').mockImplementation((path: PathOrFileDescriptor) => {\n            if (path === 'package.json') {\n                return JSON.stringify(expectedPackage);\n            }\n            return '';\n        });\n\n        const actualReadPackage = ExpandWildcard.readPackage();\n        expect(actualReadPackage).toEqual(expectedPackage);\n    });\n\n    it('can handle errors reading package', () => {\n        vi.spyOn(fs, 'readFileSync').mockImplementation(() => {\n            throw new Error('Error reading package');\n        });\n\n        expect(() => ExpandWildcard.readPackage()).not.toThrow();\n        expect(ExpandWildcard.readPackage()).toEqual({});\n    });\n});\n\nit('returns same command if not an npm run command', () => {\n    const commandInfo = createCommandInfo('npm test');\n\n    expect(readDeno).not.toHaveBeenCalled();\n    expect(readPackage).not.toHaveBeenCalled();\n    expect(parser.parse(commandInfo)).toBe(commandInfo);\n});\n\nit('returns same command if not a deno task command', () => {\n    const commandInfo = createCommandInfo('deno run');\n\n    expect(readDeno).not.toHaveBeenCalled();\n    expect(readPackage).not.toHaveBeenCalled();\n    expect(parser.parse(commandInfo)).toBe(commandInfo);\n});\n\nit('returns same command if no wildcard present', () => {\n    const commandInfo = createCommandInfo('npm run foo bar');\n\n    expect(readPackage).not.toHaveBeenCalled();\n    expect(parser.parse(commandInfo)).toBe(commandInfo);\n});\n\nit('expands to nothing if no scripts exist in package.json', () => {\n    readPackage.mockReturnValue({});\n\n    expect(parser.parse(createCommandInfo('npm run foo-*-baz qux'))).toEqual([]);\n});\n\nit('expands to nothing if no tasks exist in Deno config and no scripts exist in NodeJS config', () => {\n    readDeno.mockReturnValue({});\n    readPackage.mockReturnValue({});\n\n    expect(parser.parse(createCommandInfo('deno task foo-*-baz qux'))).toEqual([]);\n});\n\ndescribe.each(['npm run', 'yarn run', 'pnpm run', 'bun run', 'node --run'])(\n    `with a '%s' prefix`,\n    (command) => {\n        it('expands to all scripts matching pattern', () => {\n            readPackage.mockReturnValue({\n                scripts: {\n                    'foo-bar-baz': '',\n                    'foo--baz': '',\n                },\n            });\n\n            expect(parser.parse(createCommandInfo(`${command} foo-*-baz qux`))).toEqual([\n                { name: 'bar', command: `${command} foo-bar-baz qux` },\n                { name: '', command: `${command} foo--baz qux` },\n            ]);\n        });\n\n        it('uses wildcard match of script as command name', () => {\n            readPackage.mockReturnValue({\n                scripts: {\n                    'watch-js': '',\n                    'watch-css': '',\n                },\n            });\n\n            expect(\n                parser.parse({\n                    name: 'watch-*',\n                    command: `${command} watch-*`,\n                }),\n            ).toEqual([\n                { name: 'js', command: `${command} watch-js` },\n                { name: 'css', command: `${command} watch-css` },\n            ]);\n        });\n\n        it('uses existing command name as prefix to the wildcard match', () => {\n            readPackage.mockReturnValue({\n                scripts: {\n                    'watch-js': '',\n                    'watch-css': '',\n                },\n            });\n\n            expect(\n                parser.parse({\n                    name: 'w:',\n                    command: `${command} watch-*`,\n                }),\n            ).toEqual([\n                { name: 'w:js', command: `${command} watch-js` },\n                { name: 'w:css', command: `${command} watch-css` },\n            ]);\n        });\n\n        it('allows negation', () => {\n            readPackage.mockReturnValue({\n                scripts: {\n                    'lint:js': '',\n                    'lint:ts': '',\n                    'lint:fix:js': '',\n                    'lint:fix:ts': '',\n                },\n            });\n\n            expect(parser.parse(createCommandInfo(`${command} lint:*(!fix)`))).toEqual([\n                { name: 'js', command: `${command} lint:js` },\n                { name: 'ts', command: `${command} lint:ts` },\n            ]);\n        });\n\n        it('caches scripts upon calls', () => {\n            readPackage.mockReturnValue({});\n\n            parser.parse(createCommandInfo(`${command} foo-*-baz qux`));\n            parser.parse(createCommandInfo(`${command} foo-*-baz qux`));\n\n            expect(readPackage).toHaveBeenCalledTimes(1);\n        });\n\n        it(\"doesn't read Deno config\", () => {\n            readPackage.mockReturnValue({});\n\n            parser.parse(createCommandInfo(`${command} foo-*-baz qux`));\n\n            expect(readDeno).not.toHaveBeenCalled();\n        });\n    },\n);\n\ndescribe(`with a 'deno task' prefix`, () => {\n    it('expands to all scripts matching pattern', () => {\n        readDeno.mockReturnValue({\n            tasks: {\n                'foo-bar-baz': '',\n                'foo--baz': '',\n            },\n        });\n        readPackage.mockReturnValue({\n            scripts: {\n                'foo-foo-baz': '',\n            },\n        });\n\n        expect(parser.parse(createCommandInfo(`deno task foo-*-baz qux`))).toEqual([\n            { name: 'bar', command: `deno task foo-bar-baz qux` },\n            { name: '', command: `deno task foo--baz qux` },\n            { name: 'foo', command: `deno task foo-foo-baz qux` },\n        ]);\n    });\n\n    it('uses wildcard match of script as command name', () => {\n        readDeno.mockReturnValue({\n            tasks: {\n                'watch-sass': '',\n            },\n        });\n        readPackage.mockReturnValue({\n            scripts: {\n                'watch-js': '',\n                'watch-css': '',\n            },\n        });\n\n        expect(\n            parser.parse({\n                name: '',\n                command: `deno task watch-*`,\n            }),\n        ).toEqual([\n            { name: 'sass', command: `deno task watch-sass` },\n            { name: 'js', command: `deno task watch-js` },\n            { name: 'css', command: `deno task watch-css` },\n        ]);\n    });\n\n    it('uses existing command name as prefix to the wildcard match', () => {\n        readDeno.mockReturnValue({\n            tasks: {\n                'watch-sass': '',\n            },\n        });\n        readPackage.mockReturnValue({\n            scripts: {\n                'watch-js': '',\n                'watch-css': '',\n            },\n        });\n\n        expect(\n            parser.parse({\n                name: 'w:',\n                command: `deno task watch-*`,\n            }),\n        ).toEqual([\n            { name: 'w:sass', command: `deno task watch-sass` },\n            { name: 'w:js', command: `deno task watch-js` },\n            { name: 'w:css', command: `deno task watch-css` },\n        ]);\n    });\n\n    it('allows negation', () => {\n        readDeno.mockReturnValue({\n            tasks: {\n                'lint:sass': '',\n                'lint:fix:sass': '',\n            },\n        });\n        readPackage.mockReturnValue({\n            scripts: {\n                'lint:js': '',\n                'lint:ts': '',\n                'lint:fix:js': '',\n                'lint:fix:ts': '',\n            },\n        });\n\n        expect(parser.parse(createCommandInfo(`deno task lint:*(!fix)`))).toEqual([\n            { name: 'sass', command: `deno task lint:sass` },\n            { name: 'js', command: `deno task lint:js` },\n            { name: 'ts', command: `deno task lint:ts` },\n        ]);\n    });\n\n    it('caches scripts upon calls', () => {\n        readDeno.mockReturnValue({});\n        readPackage.mockReturnValue({});\n\n        parser.parse(createCommandInfo(`deno task foo-*-baz qux`));\n        parser.parse(createCommandInfo(`deno task foo-*-baz qux`));\n\n        expect(readDeno).toHaveBeenCalledTimes(1);\n        expect(readPackage).toHaveBeenCalledTimes(1);\n    });\n});\n"
  },
  {
    "path": "lib/command-parser/expand-wildcard.ts",
    "content": "import fs from 'node:fs';\n\nimport { CommandInfo } from '../command.js';\nimport JSONC from '../jsonc.js';\nimport { escapeRegExp } from '../utils.js';\nimport { CommandParser } from './command-parser.js';\n\n// Matches a negative filter surrounded by '(!' and ')'.\nconst OMISSION = /\\(!([^)]+)\\)/;\n\n/**\n * Finds wildcards in 'npm/yarn/pnpm/bun run', 'node --run' and 'deno task'\n * commands and replaces them with all matching scripts in the NodeJS and Deno\n * configuration files of the current directory.\n */\nexport class ExpandWildcard implements CommandParser {\n    static readDeno() {\n        try {\n            let json: string = '{}';\n\n            if (fs.existsSync('deno.json')) {\n                json = fs.readFileSync('deno.json', { encoding: 'utf-8' });\n            } else if (fs.existsSync('deno.jsonc')) {\n                json = fs.readFileSync('deno.jsonc', { encoding: 'utf-8' });\n            }\n\n            return JSONC.parse(json);\n        } catch {\n            return {};\n        }\n    }\n\n    static readPackage() {\n        try {\n            const json = fs.readFileSync('package.json', { encoding: 'utf-8' });\n            return JSON.parse(json);\n        } catch {\n            return {};\n        }\n    }\n\n    private packageScripts?: string[];\n    private denoTasks?: string[];\n\n    constructor(\n        private readonly readDeno = ExpandWildcard.readDeno,\n        private readonly readPackage = ExpandWildcard.readPackage,\n    ) {}\n\n    private relevantScripts(command: string): string[] {\n        if (!this.packageScripts) {\n            this.packageScripts = Object.keys(this.readPackage().scripts || {});\n        }\n\n        if (command === 'deno task') {\n            if (!this.denoTasks) {\n                // If Deno tries to run a task that doesn't exist,\n                // it can fall back to running a script with the same name.\n                // Therefore, the actual list of tasks is the union of the tasks and scripts.\n                this.denoTasks = [\n                    ...Object.keys(this.readDeno().tasks || {}),\n                    ...this.packageScripts,\n                ];\n            }\n\n            return this.denoTasks;\n        }\n\n        return this.packageScripts;\n    }\n\n    parse(commandInfo: CommandInfo) {\n        // We expect one of the following patterns:\n        // - <npm|yarn|pnpm|bun> run <script> [args]\n        // - node --run <script> [args]\n        // - deno task <script> [args]\n        const [, command, scriptGlob, args] =\n            /((?:npm|yarn|pnpm|bun) run|node --run|deno task) (\\S+)([^&]*)/.exec(\n                commandInfo.command,\n            ) || [];\n\n        const wildcardPosition = (scriptGlob || '').indexOf('*');\n\n        // If the regex didn't match an npm script, or it has no wildcard,\n        // then we have nothing to do here\n        if (wildcardPosition === -1) {\n            return commandInfo;\n        }\n\n        const [, omission] = OMISSION.exec(scriptGlob) || [];\n        const scriptGlobSansOmission = scriptGlob.replace(OMISSION, '');\n        const preWildcard = escapeRegExp(scriptGlobSansOmission.slice(0, wildcardPosition));\n        const postWildcard = escapeRegExp(scriptGlobSansOmission.slice(wildcardPosition + 1));\n        const wildcardRegex = new RegExp(`^${preWildcard}(.*?)${postWildcard}$`);\n        // If 'commandInfo.name' doesn't match 'scriptGlob', this means a custom name\n        // has been specified and thus becomes the prefix (as described in the README).\n        const prefix = commandInfo.name !== scriptGlob ? commandInfo.name : '';\n\n        const commands: CommandInfo[] = [];\n\n        for (const script of this.relevantScripts(command)) {\n            if (omission && new RegExp(omission).test(script)) {\n                continue;\n            }\n\n            const result = wildcardRegex.exec(script);\n            const match = result?.[1];\n            if (match !== undefined) {\n                commands.push({\n                    ...commandInfo,\n                    command: `${command} ${script}${args}`,\n                    // Will use an empty command name if no prefix has been specified and\n                    // the wildcard match is empty, e.g. if `npm:watch-*` matches `npm run watch-`.\n                    name: prefix + match,\n                });\n            }\n        }\n\n        return commands;\n    }\n}\n"
  },
  {
    "path": "lib/command-parser/strip-quotes.spec.ts",
    "content": "import { expect, it } from 'vitest';\n\nimport { CommandInfo } from '../command.js';\nimport { StripQuotes } from './strip-quotes.js';\n\nconst parser = new StripQuotes();\n\nconst createCommandInfo = (command: string): CommandInfo => ({\n    command,\n    name: '',\n});\n\nit('returns command as is if no single/double quote at the beginning', () => {\n    const commandInfo = createCommandInfo('echo foo');\n    expect(parser.parse(commandInfo)).toEqual(commandInfo);\n});\n\nit('strips single quotes', () => {\n    const commandInfo = createCommandInfo(\"'echo foo'\");\n    expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo foo' });\n});\n\nit('strips double quotes', () => {\n    const commandInfo = createCommandInfo('\"echo foo\"');\n    expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo foo' });\n});\n\nit('does not remove quotes if they are unbalanced', () => {\n    let commandInfo = createCommandInfo('\"echo foo');\n    expect(parser.parse(commandInfo)).toEqual(commandInfo);\n\n    commandInfo = createCommandInfo(\"echo foo'\");\n    expect(parser.parse(commandInfo)).toEqual(commandInfo);\n\n    commandInfo = createCommandInfo('\"echo foo\\'');\n    expect(parser.parse(commandInfo)).toEqual(commandInfo);\n});\n"
  },
  {
    "path": "lib/command-parser/strip-quotes.ts",
    "content": "import { CommandInfo } from '../command.js';\nimport { CommandParser } from './command-parser.js';\n\n/**\n * Strips quotes around commands so that they can run on the current shell.\n */\nexport class StripQuotes implements CommandParser {\n    parse(commandInfo: CommandInfo) {\n        let { command } = commandInfo;\n\n        // Removes the quotes surrounding a command.\n        if (/^\".+?\"$/.test(command) || /^'.+?'$/.test(command)) {\n            command = command.slice(1, command.length - 1);\n        }\n\n        return { ...commandInfo, command };\n    }\n}\n"
  },
  {
    "path": "lib/command.spec.ts",
    "content": "import { Buffer } from 'node:buffer';\nimport { SendHandle, SpawnOptions } from 'node:child_process';\nimport { EventEmitter } from 'node:events';\nimport { Readable, Writable } from 'node:stream';\n\nimport { subscribeSpyTo } from '@hirez_io/observer-spy';\nimport Rx from 'rxjs';\nimport { beforeEach, describe, expect, it, Mock, vi } from 'vitest';\n\nimport {\n    ChildProcess,\n    CloseEvent,\n    Command,\n    CommandInfo,\n    KillProcess,\n    SpawnCommand,\n} from './command.js';\n\ninterface CommandValues {\n    error: unknown;\n    close: CloseEvent;\n    timer: unknown[];\n}\n\nlet process: ChildProcess;\nlet sendMessage: Mock;\nlet spawn: Mock<SpawnCommand>;\nlet killProcess: KillProcess;\n\nconst IPC_FD = 3;\n\nbeforeEach(() => {\n    sendMessage = vi.fn();\n    process = new (class extends EventEmitter {\n        readonly pid = 1;\n        send = sendMessage;\n        readonly stdout = new Readable({\n            read() {\n                // do nothing\n            },\n        });\n        readonly stderr = new Readable({\n            read() {\n                // do nothing\n            },\n        });\n        readonly stdin = new Writable({\n            write() {\n                // do nothing\n            },\n        });\n    })();\n    spawn = vi.fn().mockReturnValue(process);\n    killProcess = vi.fn();\n});\n\nconst createCommand = (overrides?: Partial<CommandInfo>, spawnOpts: SpawnOptions = {}) => {\n    const command = new Command(\n        { index: 0, name: '', command: 'echo foo', ...overrides },\n        spawnOpts,\n        spawn,\n        killProcess,\n    );\n\n    let error: unknown;\n    let close: CloseEvent;\n    const timer = subscribeSpyTo(command.timer);\n    const finished = subscribeSpyTo(\n        new Rx.Observable((observer) => {\n            // First event in both subjects means command has finished\n            command.error.subscribe({\n                next: (value) => {\n                    error = value;\n                    observer.complete();\n                },\n            });\n            command.close.subscribe({\n                next: (value) => {\n                    close = value;\n                    observer.complete();\n                },\n            });\n        }),\n    );\n    const values = async (): Promise<CommandValues> => {\n        await finished.onComplete();\n        return { error, close, timer: timer.getValues() };\n    };\n\n    return { command, values };\n};\n\nit('has stopped state by default', () => {\n    const { command } = createCommand();\n    expect(command.state).toBe('stopped');\n});\n\ndescribe('#start()', () => {\n    it('spawns process with given command and options', () => {\n        const { command } = createCommand({}, { detached: true });\n        command.start();\n\n        expect(spawn).toHaveBeenCalledExactlyOnceWith(command.command, { detached: true });\n    });\n\n    it('sets stdin, process and PID', () => {\n        const { command } = createCommand();\n        command.start();\n\n        expect(command.process).toBe(process);\n        expect(command.pid).toBe(process.pid);\n        expect(command.stdin).toBe(process.stdin);\n    });\n\n    it('handles process with no stdin', () => {\n        process.stdin = null;\n        const { command } = createCommand();\n        command.start();\n\n        expect(command.stdin).toBe(undefined);\n    });\n\n    it('changes state to started', () => {\n        const { command } = createCommand();\n        const spy = subscribeSpyTo(command.stateChange);\n        command.start();\n        expect(command.state).toBe('started');\n        expect(spy.getFirstValue()).toBe('started');\n    });\n\n    describe('on errors', () => {\n        it('changes state to errored', () => {\n            const { command } = createCommand();\n            command.start();\n\n            const spy = subscribeSpyTo(command.stateChange);\n            process.emit('error', 'foo');\n            expect(command.state).toBe('errored');\n            expect(spy.getFirstValue()).toBe('errored');\n        });\n\n        it('shares to the error stream', async () => {\n            const { command, values } = createCommand();\n            command.start();\n            process.emit('error', 'foo');\n            const { error } = await values();\n\n            expect(error).toBe('foo');\n            expect(command.process).toBeUndefined();\n        });\n\n        it('shares start and error timing events to the timing stream', async () => {\n            const { command, values } = createCommand();\n            const startDate = new Date();\n            const endDate = new Date(startDate.getTime() + 1000);\n            vi.spyOn(Date, 'now')\n                .mockReturnValueOnce(startDate.getTime())\n                .mockReturnValueOnce(endDate.getTime());\n            command.start();\n            process.emit('error', 0, null);\n            const { timer } = await values();\n\n            expect(timer[0]).toEqual({ startDate, endDate: undefined });\n            expect(timer[1]).toEqual({ startDate, endDate });\n        });\n    });\n\n    describe('on close', () => {\n        it('changes state to exited', () => {\n            const { command } = createCommand();\n            command.start();\n\n            const spy = subscribeSpyTo(command.stateChange);\n            process.emit('close', 0, null);\n            expect(command.state).toBe('exited');\n            expect(spy.getFirstValue()).toBe('exited');\n        });\n\n        it('does not change state if there was an error', () => {\n            const { command } = createCommand();\n            command.start();\n            process.emit('error', 'foo');\n\n            const spy = subscribeSpyTo(command.stateChange);\n            process.emit('close', 0, null);\n            expect(command.state).toBe('errored');\n            expect(spy.getValuesLength()).toBe(0);\n        });\n\n        it('shares start and close timing events to the timing stream', async () => {\n            const { command, values } = createCommand();\n            const startDate = new Date();\n            const endDate = new Date(startDate.getTime() + 1000);\n            vi.spyOn(Date, 'now')\n                .mockReturnValueOnce(startDate.getTime())\n                .mockReturnValueOnce(endDate.getTime());\n            command.start();\n            process.emit('close', 0, null);\n            const { timer } = await values();\n\n            expect(timer[0]).toEqual({ startDate, endDate: undefined });\n            expect(timer[1]).toEqual({ startDate, endDate });\n        });\n\n        it('shares to the close stream with exit code', async () => {\n            const { command, values } = createCommand();\n            command.start();\n            process.emit('close', 0, null);\n            const { close } = await values();\n\n            expect(close).toMatchObject({ exitCode: 0, killed: false });\n            expect(command.process).toBeUndefined();\n        });\n\n        it('shares to the close stream with signal', async () => {\n            const { command, values } = createCommand();\n            command.start();\n            process.emit('close', null, 'SIGKILL');\n            const { close } = await values();\n\n            expect(close).toMatchObject({ exitCode: 'SIGKILL', killed: false });\n        });\n\n        it('shares to the close stream with timing information', async () => {\n            const { command, values } = createCommand();\n            const startDate = new Date();\n            const endDate = new Date(startDate.getTime() + 1000);\n            vi.spyOn(Date, 'now')\n                .mockReturnValueOnce(startDate.getTime())\n                .mockReturnValueOnce(endDate.getTime());\n            vi.spyOn(globalThis.process, 'hrtime')\n                .mockReturnValueOnce([0, 0])\n                .mockReturnValueOnce([1, 1e8]);\n            command.start();\n            process.emit('close', null, 'SIGKILL');\n            const { close } = await values();\n\n            expect(close.timings).toStrictEqual({\n                startDate,\n                endDate,\n                durationSeconds: 1.1,\n            });\n        });\n\n        it('shares to the close stream with command info', async () => {\n            const commandInfo = {\n                command: 'cmd',\n                name: 'name',\n                prefixColor: 'green',\n                env: { VAR: 'yes' },\n            };\n            const { command, values } = createCommand(commandInfo);\n            command.start();\n            process.emit('close', 0, null);\n            const { close } = await values();\n\n            expect(close.command).toEqual(expect.objectContaining(commandInfo));\n            expect(close.killed).toBe(false);\n        });\n    });\n\n    it('shares stdout to the stdout stream', async () => {\n        const { command } = createCommand();\n        const stdout = Rx.firstValueFrom(command.stdout);\n        command.start();\n        process.stdout?.emit('data', Buffer.from('hello'));\n\n        expect((await stdout).toString()).toBe('hello');\n    });\n\n    it('shares stderr to the stdout stream', async () => {\n        const { command } = createCommand();\n        const stderr = Rx.firstValueFrom(command.stderr);\n        command.start();\n        process.stderr?.emit('data', Buffer.from('dang'));\n\n        expect((await stderr).toString()).toBe('dang');\n    });\n\n    describe('on incoming messages', () => {\n        it('does not share to the incoming messages stream, if IPC is disabled', () => {\n            const { command } = createCommand();\n            const spy = subscribeSpyTo(command.messages.incoming);\n            command.start();\n\n            process.emit('message', {});\n            expect(spy.getValuesLength()).toBe(0);\n        });\n\n        it('shares to the incoming messages stream, if IPC is enabled', () => {\n            const { command } = createCommand({ ipc: IPC_FD });\n            const spy = subscribeSpyTo(command.messages.incoming);\n            command.start();\n\n            const message1 = {};\n            process.emit('message', message1, undefined);\n\n            const message2 = {};\n            const handle = {} as SendHandle;\n            process.emit('message', message2, handle);\n\n            expect(spy.getValuesLength()).toBe(2);\n            expect(spy.getValueAt(0)).toEqual({ message: message1, handle: undefined });\n            expect(spy.getValueAt(1)).toEqual({ message: message2, handle });\n        });\n    });\n\n    describe('on outgoing messages', () => {\n        it('calls onSent with an error if the process does not have IPC enabled', () => {\n            const { command } = createCommand({ ipc: IPC_FD });\n            command.start();\n\n            Object.assign(process, {\n                // The TS types don't assume `send` can be undefined,\n                // despite the Node docs saying so\n                send: undefined,\n            });\n\n            const onSent = vi.fn();\n            command.messages.outgoing.next({ message: {}, onSent });\n            expect(onSent).toHaveBeenCalledWith(expect.any(Error));\n        });\n\n        it('sends the message to the process', () => {\n            const { command } = createCommand({ ipc: IPC_FD });\n            command.start();\n\n            const message1 = {};\n            command.messages.outgoing.next({ message: message1, onSent() {} });\n\n            const message2 = {};\n            const handle = {} as SendHandle;\n            command.messages.outgoing.next({ message: message2, handle, onSent() {} });\n\n            const message3 = {};\n            const options = {};\n            command.messages.outgoing.next({ message: message3, options, onSent() {} });\n\n            expect(process.send).toHaveBeenCalledTimes(3);\n            expect(process.send).toHaveBeenNthCalledWith(\n                1,\n                message1,\n                undefined,\n                undefined,\n                expect.any(Function),\n            );\n            expect(process.send).toHaveBeenNthCalledWith(\n                2,\n                message2,\n                handle,\n                undefined,\n                expect.any(Function),\n            );\n            expect(process.send).toHaveBeenNthCalledWith(\n                3,\n                message3,\n                undefined,\n                options,\n                expect.any(Function),\n            );\n        });\n\n        it('sends the message to the process, if it starts late', () => {\n            const { command } = createCommand({ ipc: IPC_FD });\n            command.messages.outgoing.next({ message: {}, onSent() {} });\n            expect(process.send).not.toHaveBeenCalled();\n\n            command.start();\n            expect(process.send).toHaveBeenCalled();\n        });\n\n        it('calls onSent with the result of sending the message', () => {\n            const { command } = createCommand({ ipc: IPC_FD });\n            command.start();\n\n            const onSent = vi.fn();\n            command.messages.outgoing.next({ message: {}, onSent });\n            expect(onSent).not.toHaveBeenCalled();\n\n            sendMessage.mock.calls[0][3]();\n            expect(onSent).toHaveBeenCalledWith(undefined);\n\n            const error = new Error('test');\n            sendMessage.mock.calls[0][3](error);\n            expect(onSent).toHaveBeenCalledWith(error);\n        });\n    });\n});\n\ndescribe('#send()', () => {\n    it('throws if IPC is not set up', () => {\n        const { command } = createCommand();\n        const fn = () => command.send({});\n        expect(fn).toThrow();\n    });\n\n    it('pushes the message on the outgoing messages stream', () => {\n        const { command } = createCommand({ ipc: IPC_FD });\n        const spy = subscribeSpyTo(command.messages.outgoing);\n\n        const message1 = { foo: true };\n        command.send(message1);\n\n        const message2 = { bar: 123 };\n        const handle = {} as SendHandle;\n        command.send(message2, handle);\n\n        const message3 = { baz: 'yes' };\n        const options = {};\n        command.send(message3, undefined, options);\n\n        expect(spy.getValuesLength()).toBe(3);\n        expect(spy.getValueAt(0)).toMatchObject({\n            message: message1,\n            handle: undefined,\n            options: undefined,\n        });\n        expect(spy.getValueAt(1)).toMatchObject({ message: message2, handle, options: undefined });\n        expect(spy.getValueAt(2)).toMatchObject({ message: message3, handle: undefined, options });\n    });\n\n    it('resolves when onSent callback is called with no arguments', async () => {\n        const { command } = createCommand({ ipc: IPC_FD });\n        const spy = subscribeSpyTo(command.messages.outgoing);\n        const promise = command.send({});\n        spy.getFirstValue().onSent();\n        await expect(promise).resolves.toBeUndefined();\n    });\n\n    it('rejects when onSent callback is called with an argument', async () => {\n        const { command } = createCommand({ ipc: IPC_FD });\n        const spy = subscribeSpyTo(command.messages.outgoing);\n        const promise = command.send({});\n        spy.getFirstValue().onSent('foo');\n        await expect(promise).rejects.toBe('foo');\n    });\n});\n\ndescribe('#kill()', () => {\n    let createdCommand: { command: Command; values: () => Promise<CommandValues> };\n    beforeEach(() => {\n        createdCommand = createCommand();\n    });\n\n    it('kills process', () => {\n        createdCommand.command.start();\n        createdCommand.command.kill();\n\n        expect(killProcess).toHaveBeenCalledExactlyOnceWith(createdCommand.command.pid, undefined);\n    });\n\n    it('kills process with some signal', () => {\n        createdCommand.command.start();\n        createdCommand.command.kill('SIGKILL');\n\n        expect(killProcess).toHaveBeenCalledExactlyOnceWith(createdCommand.command.pid, 'SIGKILL');\n    });\n\n    it('does not try to kill inexistent process', () => {\n        createdCommand.command.start();\n        process.emit('error');\n        createdCommand.command.kill();\n\n        expect(killProcess).not.toHaveBeenCalled();\n    });\n\n    it('marks the command as killed', async () => {\n        createdCommand.command.start();\n        createdCommand.command.kill();\n        process.emit('close', 1, null);\n        const { close } = await createdCommand.values();\n\n        expect(close).toMatchObject({ exitCode: 1, killed: true });\n    });\n});\n\ndescribe('.canKill()', () => {\n    it('returns whether command has both PID and process', () => {\n        const { command } = createCommand();\n        expect(Command.canKill(command)).toBe(false);\n\n        command.pid = 1;\n        expect(Command.canKill(command)).toBe(false);\n\n        command.process = process;\n        expect(Command.canKill(command)).toBe(true);\n    });\n});\n"
  },
  {
    "path": "lib/command.ts",
    "content": "import { Buffer } from 'node:buffer';\nimport {\n    ChildProcess as BaseChildProcess,\n    MessageOptions,\n    SendHandle,\n    SpawnOptions,\n} from 'node:child_process';\nimport process from 'node:process';\nimport { EventEmitter, Writable } from 'node:stream';\n\nimport Rx from 'rxjs';\n\n/**\n * Identifier for a command; if string, it's the command's name, if number, it's the index.\n */\nexport type CommandIdentifier = string | number;\n\nexport interface CommandInfo {\n    /**\n     * Command's name.\n     */\n    name: string;\n\n    /**\n     * Which command line the command has.\n     */\n    command: string;\n\n    /**\n     * Which environment variables should the spawned process have.\n     */\n    env?: Record<string, unknown>;\n\n    /**\n     * The current working directory of the process when spawned.\n     */\n    cwd?: string;\n\n    /**\n     * Color to use on prefix of the command.\n     */\n    prefixColor?: string;\n\n    /**\n     * Whether sending of messages to/from this command (also known as \"inter-process communication\")\n     * should be enabled, and using which file descriptor number.\n     *\n     * If set, must be > 2.\n     */\n    ipc?: number;\n\n    /**\n     * Output command in raw format.\n     */\n    raw?: boolean;\n}\n\nexport interface CloseEvent {\n    command: CommandInfo;\n\n    /**\n     * The command's index among all commands ran.\n     */\n    index: number;\n\n    /**\n     * Whether the command exited because it was killed.\n     */\n    killed: boolean;\n\n    /**\n     * The exit code or signal for the command.\n     */\n    exitCode: string | number;\n\n    timings: {\n        startDate: Date;\n        endDate: Date;\n        durationSeconds: number;\n    };\n}\n\nexport interface TimerEvent {\n    startDate: Date;\n    endDate?: Date;\n}\n\nexport interface MessageEvent {\n    message: object;\n    handle?: SendHandle;\n}\n\ninterface OutgoingMessageEvent extends MessageEvent {\n    options?: MessageOptions;\n    onSent: (error?: unknown) => void;\n}\n\n/**\n * Subtype of NodeJS's child_process including only what's actually needed for a command to work.\n */\nexport type ChildProcess = EventEmitter &\n    Pick<BaseChildProcess, 'pid' | 'stdin' | 'stdout' | 'stderr' | 'send'>;\n\n/**\n * Interface for a function that must kill the process with `pid`, optionally sending `signal` to it.\n */\nexport type KillProcess = (pid: number, signal?: string) => void;\n\n/**\n * Interface for a function that spawns a command and returns its child process instance.\n */\nexport type SpawnCommand = (command: string, options: SpawnOptions) => ChildProcess;\n\n/**\n * The state of a command.\n *\n * - `stopped`: command was never started\n * - `started`: command is currently running\n * - `errored`: command failed spawning\n * - `exited`: command is not running anymore, e.g. it received a close event\n */\ntype CommandState = 'stopped' | 'started' | 'errored' | 'exited';\n\nexport class Command implements CommandInfo {\n    private readonly killProcess: KillProcess;\n    private readonly spawn: SpawnCommand;\n    private readonly spawnOpts: SpawnOptions;\n    readonly index: number;\n\n    /** @inheritdoc */\n    readonly name: string;\n\n    /** @inheritdoc */\n    readonly command: string;\n\n    /** @inheritdoc */\n    readonly prefixColor?: string;\n\n    /** @inheritdoc */\n    readonly env: Record<string, unknown>;\n\n    /** @inheritdoc */\n    readonly cwd?: string;\n\n    /** @inheritdoc */\n    readonly ipc?: number;\n\n    readonly close = new Rx.Subject<CloseEvent>();\n    readonly error = new Rx.Subject<unknown>();\n    readonly stdout = new Rx.Subject<Buffer>();\n    readonly stderr = new Rx.Subject<Buffer>();\n    readonly timer = new Rx.Subject<TimerEvent>();\n\n    /**\n     * A stream of changes to the `#state` property.\n     *\n     * Note that the command never goes back to the `stopped` state, therefore it's not a value\n     * that's emitted by this stream.\n     */\n    readonly stateChange = new Rx.Subject<Exclude<CommandState, 'stopped'>>();\n    readonly messages = {\n        incoming: new Rx.Subject<MessageEvent>(),\n        outgoing: new Rx.ReplaySubject<OutgoingMessageEvent>(),\n    };\n\n    process?: ChildProcess;\n\n    // TODO: Should exit/error/stdio subscriptions be added here?\n    private subscriptions: readonly Rx.Subscription[] = [];\n    stdin?: Writable;\n    pid?: number;\n    killed = false;\n    exited = false;\n\n    state: CommandState = 'stopped';\n\n    constructor(\n        { index, name, command, prefixColor, env, cwd, ipc }: CommandInfo & { index: number },\n        spawnOpts: SpawnOptions,\n        spawn: SpawnCommand,\n        killProcess: KillProcess,\n    ) {\n        this.index = index;\n        this.name = name;\n        this.command = command;\n        this.prefixColor = prefixColor;\n        this.env = env || {};\n        this.cwd = cwd;\n        this.ipc = ipc;\n        this.killProcess = killProcess;\n        this.spawn = spawn;\n        this.spawnOpts = spawnOpts;\n    }\n\n    /**\n     * Starts this command, piping output, error and close events onto the corresponding observables.\n     */\n    start() {\n        const child = this.spawn(this.command, this.spawnOpts);\n        this.changeState('started');\n        this.process = child;\n        this.pid = child.pid;\n        const startDate = new Date(Date.now());\n        const highResStartTime = process.hrtime();\n        this.timer.next({ startDate });\n\n        this.subscriptions = [...this.maybeSetupIPC(child)];\n        Rx.fromEvent(child, 'error').subscribe((event) => {\n            this.cleanUp();\n            const endDate = new Date(Date.now());\n            this.timer.next({ startDate, endDate });\n            this.error.next(event);\n            this.changeState('errored');\n        });\n        Rx.fromEvent(child, 'close')\n            .pipe(Rx.map((event) => event as [number | null, NodeJS.Signals | null]))\n            .subscribe(([exitCode, signal]) => {\n                this.cleanUp();\n\n                // Don't override error event\n                if (this.state !== 'errored') {\n                    this.changeState('exited');\n                }\n\n                const endDate = new Date(Date.now());\n                this.timer.next({ startDate, endDate });\n                const [durationSeconds, durationNanoSeconds] = process.hrtime(highResStartTime);\n                this.close.next({\n                    command: this,\n                    index: this.index,\n                    exitCode: exitCode ?? String(signal),\n                    killed: this.killed,\n                    timings: {\n                        startDate,\n                        endDate,\n                        durationSeconds: durationSeconds + durationNanoSeconds / 1e9,\n                    },\n                });\n            });\n        if (child.stdout) {\n            pipeTo(\n                Rx.fromEvent(child.stdout, 'data').pipe(Rx.map((event) => event as Buffer)),\n                this.stdout,\n            );\n        }\n        if (child.stderr) {\n            pipeTo(\n                Rx.fromEvent(child.stderr, 'data').pipe(Rx.map((event) => event as Buffer)),\n                this.stderr,\n            );\n        }\n        this.stdin = child.stdin || undefined;\n    }\n\n    private changeState(state: Exclude<CommandState, 'stopped'>) {\n        this.state = state;\n        this.stateChange.next(state);\n    }\n\n    private maybeSetupIPC(child: ChildProcess) {\n        if (!this.ipc) {\n            return [];\n        }\n\n        return [\n            pipeTo(\n                Rx.fromEvent(child, 'message').pipe(\n                    Rx.map((event) => {\n                        const [message, handle] = event as [object, SendHandle | undefined];\n                        return { message, handle };\n                    }),\n                ),\n                this.messages.incoming,\n            ),\n            this.messages.outgoing.subscribe((message) => {\n                if (!child.send) {\n                    return message.onSent(new Error('Command does not have an IPC channel'));\n                }\n\n                child.send(message.message, message.handle, message.options, (error) => {\n                    message.onSent(error);\n                });\n            }),\n        ];\n    }\n\n    /**\n     * Sends a message to the underlying process once it starts.\n     *\n     * @throws  If the command doesn't have an IPC channel enabled\n     * @returns Promise that resolves when the message is sent,\n     *          or rejects if it fails to deliver the message.\n     */\n    send(message: object, handle?: SendHandle, options?: MessageOptions): Promise<void> {\n        if (this.ipc == null) {\n            throw new Error('Command IPC is disabled');\n        }\n        return new Promise((resolve, reject) => {\n            this.messages.outgoing.next({\n                message,\n                handle,\n                options,\n                onSent(error) {\n                    if (error) {\n                        reject(error);\n                    } else {\n                        resolve();\n                    }\n                },\n            });\n        });\n    }\n\n    /**\n     * Kills this command, optionally specifying a signal to send to it.\n     */\n    kill(code?: string) {\n        if (Command.canKill(this)) {\n            this.killed = true;\n            this.killProcess(this.pid, code);\n        }\n    }\n\n    private cleanUp() {\n        this.subscriptions?.forEach((sub) => sub.unsubscribe());\n        this.messages.outgoing = new Rx.ReplaySubject();\n        this.process = undefined;\n    }\n\n    /**\n     * Detects whether a command can be killed.\n     *\n     * Also works as a type guard on the input `command`.\n     */\n    static canKill(command: Command): command is Command & { pid: number; process: ChildProcess } {\n        return !!command.pid && !!command.process;\n    }\n}\n\n/**\n * Pipes all events emitted by `stream` into `subject`.\n */\nfunction pipeTo<T>(stream: Rx.Observable<T>, subject: Rx.Subject<T>) {\n    return stream.subscribe((event) => subject.next(event));\n}\n"
  },
  {
    "path": "lib/completion-listener.spec.ts",
    "content": "import { getEventListeners } from 'node:events';\n\nimport { TestScheduler } from 'rxjs/testing';\nimport { beforeEach, describe, expect, it, vi } from 'vitest';\n\nimport { createFakeCloseEvent, FakeCommand } from './__fixtures__/fake-command.js';\nimport { CloseEvent } from './command.js';\nimport { CompletionListener, SuccessCondition } from './completion-listener.js';\n\nlet commands: FakeCommand[];\nlet scheduler: TestScheduler;\n\nbeforeEach(() => {\n    commands = [\n        new FakeCommand('foo', 'echo', 0),\n        new FakeCommand('bar', 'echo', 1),\n        new FakeCommand('baz', 'echo', 2),\n    ];\n    scheduler = new TestScheduler(() => true);\n});\n\nconst createController = (successCondition?: SuccessCondition) =>\n    new CompletionListener({\n        successCondition,\n        scheduler,\n    });\n\nconst emitFakeCloseEvent = (command: FakeCommand, event?: Partial<CloseEvent>) => {\n    const fakeEvent = createFakeCloseEvent({ ...event, command, index: command.index });\n    command.state = 'exited';\n    command.close.next(fakeEvent);\n    return fakeEvent;\n};\n\nconst flushPromises = () => new Promise((resolve) => setTimeout(resolve, 0));\n\ndescribe('listen', () => {\n    it('resolves when there are no commands', async () => {\n        const result = createController().listen([]);\n        await expect(result).resolves.toHaveLength(0);\n    });\n\n    it('completes only when commands emit a close event, returns close event', async () => {\n        const abortCtrl = new AbortController();\n        const result = createController('all').listen(commands, abortCtrl.signal);\n\n        commands[0].state = 'started';\n        abortCtrl.abort();\n\n        const event = emitFakeCloseEvent(commands[0]);\n        scheduler.flush();\n\n        await expect(result).resolves.toHaveLength(1);\n        await expect(result).resolves.toEqual([event]);\n    });\n\n    it('completes when abort signal is received and command is stopped, returns nothing', async () => {\n        const abortCtrl = new AbortController();\n        // Use success condition = first to test index access when there are no close events\n        const result = createController('first').listen([new FakeCommand()], abortCtrl.signal);\n\n        abortCtrl.abort();\n        scheduler.flush();\n\n        await expect(result).resolves.toHaveLength(0);\n    });\n\n    it('does not leak memory when listening for abort signals', () => {\n        const abortCtrl = new AbortController();\n        createController().listen(\n            Array.from({ length: 10 }, () => new FakeCommand()),\n            abortCtrl.signal,\n        );\n        expect(getEventListeners(abortCtrl.signal, 'abort')).toHaveLength(1);\n    });\n\n    it('check for success once all commands have emitted at least a single close event', async () => {\n        const finallyCallback = vi.fn();\n        const result = createController().listen(commands).finally(finallyCallback);\n\n        // Emitting multiple close events to mimic calling command `kill/start` APIs.\n        emitFakeCloseEvent(commands[0]);\n        emitFakeCloseEvent(commands[0]);\n        emitFakeCloseEvent(commands[0]);\n\n        scheduler.flush();\n        // A broken implementation will have called finallyCallback only after flushing promises\n        await flushPromises();\n        expect(finallyCallback).not.toHaveBeenCalled();\n\n        emitFakeCloseEvent(commands[1]);\n        emitFakeCloseEvent(commands[2]);\n\n        scheduler.flush();\n\n        await expect(result).resolves.toEqual(expect.anything());\n        expect(finallyCallback).toHaveBeenCalled();\n    });\n\n    it('takes last event emitted from each command', async () => {\n        const result = createController().listen(commands);\n\n        emitFakeCloseEvent(commands[0], { exitCode: 0 });\n        emitFakeCloseEvent(commands[0], { exitCode: 1 });\n        emitFakeCloseEvent(commands[1], { exitCode: 0 });\n        emitFakeCloseEvent(commands[2], { exitCode: 0 });\n\n        scheduler.flush();\n\n        await expect(result).rejects.toEqual(expect.anything());\n    });\n\n    it('waits for manually restarted events to close', async () => {\n        const finallyCallback = vi.fn();\n        const result = createController().listen(commands).finally(finallyCallback);\n\n        emitFakeCloseEvent(commands[0]);\n        commands[0].state = 'started';\n        emitFakeCloseEvent(commands[1]);\n        emitFakeCloseEvent(commands[2]);\n\n        scheduler.flush();\n        // A broken implementation will have called finallyCallback only after flushing promises\n        await flushPromises();\n        expect(finallyCallback).not.toHaveBeenCalled();\n\n        commands[0].state = 'exited';\n        emitFakeCloseEvent(commands[0]);\n        scheduler.flush();\n\n        await expect(result).resolves.toEqual(expect.anything());\n        expect(finallyCallback).toHaveBeenCalled();\n    });\n});\n\ndescribe('detect commands exit conditions', () => {\n    describe('with default success condition set', () => {\n        it('succeeds if all processes exited with code 0', () => {\n            const result = createController().listen(commands);\n\n            commands[0].close.next(createFakeCloseEvent({ exitCode: 0 }));\n            commands[1].close.next(createFakeCloseEvent({ exitCode: 0 }));\n            commands[2].close.next(createFakeCloseEvent({ exitCode: 0 }));\n\n            scheduler.flush();\n\n            return expect(result).resolves.toEqual(expect.anything());\n        });\n\n        it('fails if one of the processes exited with non-0 code', () => {\n            const result = createController().listen(commands);\n\n            commands[0].close.next(createFakeCloseEvent({ exitCode: 0 }));\n            commands[1].close.next(createFakeCloseEvent({ exitCode: 1 }));\n            commands[2].close.next(createFakeCloseEvent({ exitCode: 0 }));\n\n            scheduler.flush();\n\n            return expect(result).rejects.toEqual(expect.anything());\n        });\n    });\n\n    describe('with success condition set to first', () => {\n        it('succeeds if first process to exit has code 0', () => {\n            const result = createController('first').listen(commands);\n\n            commands[1].close.next(createFakeCloseEvent({ exitCode: 0 }));\n            commands[0].close.next(createFakeCloseEvent({ exitCode: 1 }));\n            commands[2].close.next(createFakeCloseEvent({ exitCode: 1 }));\n\n            scheduler.flush();\n\n            return expect(result).resolves.toEqual(expect.anything());\n        });\n\n        it('fails if first process to exit has non-0 code', () => {\n            const result = createController('first').listen(commands);\n\n            commands[1].close.next(createFakeCloseEvent({ exitCode: 1 }));\n            commands[0].close.next(createFakeCloseEvent({ exitCode: 0 }));\n            commands[2].close.next(createFakeCloseEvent({ exitCode: 0 }));\n\n            scheduler.flush();\n\n            return expect(result).rejects.toEqual(expect.anything());\n        });\n    });\n\n    describe('with success condition set to last', () => {\n        it('succeeds if last process to exit has code 0', () => {\n            const result = createController('last').listen(commands);\n\n            commands[1].close.next(createFakeCloseEvent({ exitCode: 1 }));\n            commands[0].close.next(createFakeCloseEvent({ exitCode: 0 }));\n            commands[2].close.next(createFakeCloseEvent({ exitCode: 0 }));\n\n            scheduler.flush();\n\n            return expect(result).resolves.toEqual(expect.anything());\n        });\n\n        it('fails if last process to exit has non-0 code', () => {\n            const result = createController('last').listen(commands);\n\n            commands[1].close.next(createFakeCloseEvent({ exitCode: 0 }));\n            commands[0].close.next(createFakeCloseEvent({ exitCode: 1 }));\n            commands[2].close.next(createFakeCloseEvent({ exitCode: 1 }));\n\n            scheduler.flush();\n\n            return expect(result).rejects.toEqual(expect.anything());\n        });\n    });\n\n    describe.each([\n        // Use the middle command for both cases to make it more difficult to make a mess up\n        // in the implementation cause false passes.\n        ['command-bar' as const, 'bar'],\n        ['command-1' as const, 1],\n    ])('with success condition set to %s', (condition, nameOrIndex) => {\n        it(`succeeds if command ${nameOrIndex} exits with code 0`, () => {\n            const result = createController(condition).listen(commands);\n\n            emitFakeCloseEvent(commands[0], { exitCode: 1 });\n            emitFakeCloseEvent(commands[1], { exitCode: 0 });\n            emitFakeCloseEvent(commands[2], { exitCode: 1 });\n\n            scheduler.flush();\n\n            return expect(result).resolves.toEqual(expect.anything());\n        });\n\n        it(`succeeds if all commands ${nameOrIndex} exit with code 0`, () => {\n            commands = [commands[0], commands[1], commands[1]];\n            const result = createController(condition).listen(commands);\n\n            emitFakeCloseEvent(commands[0], { exitCode: 1 });\n            emitFakeCloseEvent(commands[1], { exitCode: 0 });\n            emitFakeCloseEvent(commands[2], { exitCode: 0 });\n\n            scheduler.flush();\n\n            return expect(result).resolves.toEqual(expect.anything());\n        });\n\n        it(`fails if command ${nameOrIndex} exits with non-0 code`, () => {\n            const result = createController(condition).listen(commands);\n\n            emitFakeCloseEvent(commands[0], { exitCode: 0 });\n            emitFakeCloseEvent(commands[1], { exitCode: 1 });\n            emitFakeCloseEvent(commands[2], { exitCode: 0 });\n\n            scheduler.flush();\n\n            return expect(result).rejects.toEqual(expect.anything());\n        });\n\n        it(`fails if some commands ${nameOrIndex} exit with non-0 code`, () => {\n            const result = createController(condition).listen(commands);\n\n            emitFakeCloseEvent(commands[0], { exitCode: 1 });\n            emitFakeCloseEvent(commands[1], { exitCode: 0 });\n            emitFakeCloseEvent(commands[2], { exitCode: 1 });\n\n            scheduler.flush();\n\n            return expect(result).resolves.toEqual(expect.anything());\n        });\n\n        it(`fails if command ${nameOrIndex} doesn't exist`, () => {\n            const result = createController(condition).listen([commands[0]]);\n\n            emitFakeCloseEvent(commands[0], { exitCode: 0 });\n            scheduler.flush();\n\n            return expect(result).rejects.toEqual(expect.anything());\n        });\n    });\n\n    describe.each([\n        // Use the middle command for both cases to make it more difficult to make a mess up\n        // in the implementation cause false passes.\n        ['!command-bar' as const, 'bar'],\n        ['!command-1' as const, 1],\n    ])('with success condition set to %s', (condition, nameOrIndex) => {\n        it(`succeeds if all commands but ${nameOrIndex} exit with code 0`, () => {\n            const result = createController(condition).listen(commands);\n\n            emitFakeCloseEvent(commands[0], { exitCode: 0 });\n            emitFakeCloseEvent(commands[1], { exitCode: 1 });\n            emitFakeCloseEvent(commands[2], { exitCode: 0 });\n\n            scheduler.flush();\n\n            return expect(result).resolves.toEqual(expect.anything());\n        });\n\n        it(`fails if any commands but ${nameOrIndex} exit with non-0 code`, () => {\n            const result = createController(condition).listen(commands);\n\n            emitFakeCloseEvent(commands[0], { exitCode: 1 });\n            emitFakeCloseEvent(commands[1], { exitCode: 1 });\n            emitFakeCloseEvent(commands[2], { exitCode: 0 });\n\n            scheduler.flush();\n\n            return expect(result).rejects.toEqual(expect.anything());\n        });\n\n        it(`succeeds if command ${nameOrIndex} doesn't exist`, () => {\n            const result = createController(condition).listen([commands[0]]);\n\n            emitFakeCloseEvent(commands[0], { exitCode: 0 });\n            scheduler.flush();\n\n            return expect(result).resolves.toEqual(expect.anything());\n        });\n    });\n});\n"
  },
  {
    "path": "lib/completion-listener.ts",
    "content": "import Rx from 'rxjs';\nimport { delay, filter, map, share, switchMap, take } from 'rxjs/operators';\n\nimport { CloseEvent, Command } from './command.js';\n\n/**\n * Defines which command(s) in a list must exit successfully (with an exit code of `0`):\n *\n * - `first`: only the first specified command;\n * - `last`: only the last specified command;\n * - `all`: all commands.\n * - `command-{name|index}`: only the commands with the specified names or index.\n * - `!command-{name|index}`: all commands but the ones with the specified names or index.\n */\nexport type SuccessCondition =\n    | 'first'\n    | 'last'\n    | 'all'\n    | `command-${string | number}`\n    | `!command-${string | number}`;\n\n/**\n * Provides logic to determine whether lists of commands ran successfully.\n */\nexport class CompletionListener {\n    private readonly successCondition: SuccessCondition;\n    private readonly scheduler?: Rx.SchedulerLike;\n\n    constructor({\n        successCondition = 'all',\n        scheduler,\n    }: {\n        /**\n         * How this instance will define that a list of commands ran successfully.\n         * Defaults to `all`.\n         *\n         * @see {SuccessCondition}\n         */\n        successCondition?: SuccessCondition;\n\n        /**\n         * For testing only.\n         */\n        scheduler?: Rx.SchedulerLike;\n    }) {\n        this.successCondition = successCondition;\n        this.scheduler = scheduler;\n    }\n\n    private isSuccess(events: CloseEvent[]) {\n        if (!events.length) {\n            // When every command was aborted, consider a success.\n            return true;\n        }\n\n        if (this.successCondition === 'first') {\n            return events[0].exitCode === 0;\n        } else if (this.successCondition === 'last') {\n            return events[events.length - 1].exitCode === 0;\n        }\n\n        const commandSyntaxMatch = this.successCondition.match(/^!?command-(.+)$/);\n        if (commandSyntaxMatch == null) {\n            // If not a `command-` syntax, then it's an 'all' condition or it's treated as such.\n            return events.every(({ exitCode }) => exitCode === 0);\n        }\n\n        // Check `command-` syntax condition.\n        // Note that a command's `name` is not necessarily unique,\n        // in which case all of them must meet the success condition.\n        const nameOrIndex = commandSyntaxMatch[1];\n        const targetCommandsEvents = events.filter(\n            ({ command, index }) => command.name === nameOrIndex || index === Number(nameOrIndex),\n        );\n        if (this.successCondition.startsWith('!')) {\n            // All commands except the specified ones must exit successfully\n            return events.every(\n                (event) => targetCommandsEvents.includes(event) || event.exitCode === 0,\n            );\n        }\n        // Only the specified commands must exit successfully\n        return (\n            targetCommandsEvents.length > 0 &&\n            targetCommandsEvents.every((event) => event.exitCode === 0)\n        );\n    }\n\n    /**\n     * Given a list of commands, wait for all of them to exit and then evaluate their exit codes.\n     *\n     * @returns A Promise that resolves if the success condition is met, or rejects otherwise.\n     *          In either case, the value is a list of close events for commands that spawned.\n     *          Commands that didn't spawn are filtered out.\n     */\n    listen(commands: Command[], abortSignal?: AbortSignal): Promise<CloseEvent[]> {\n        if (!commands.length) {\n            return Promise.resolve([]);\n        }\n\n        const abort =\n            abortSignal &&\n            Rx.fromEvent(abortSignal, 'abort', { once: true }).pipe(\n                // The abort signal must happen before commands are killed, otherwise new commands\n                // might spawn. Because of this, it's not be possible to capture the close events\n                // without an immediate delay\n                delay(0, this.scheduler),\n                map(() => undefined),\n                // #502 - node might warn of too many active listeners on this object if it isn't shared,\n                // as each command subscribes to abort event over and over\n                share(),\n            );\n\n        const closeStreams = commands.map((command) =>\n            abort\n                ? // Commands that have been started must close.\n                  Rx.race(command.close, abort.pipe(filter(() => command.state === 'stopped')))\n                : command.close,\n        );\n\n        return Rx.lastValueFrom(\n            Rx.combineLatest(closeStreams).pipe(\n                filter(() => commands.every((command) => command.state !== 'started')),\n                map((events) =>\n                    events\n                        // Filter out aborts, since they cannot be sorted and are considered success condition anyways\n                        .filter((event): event is CloseEvent => event != null)\n                        // Sort according to exit time\n                        .sort(\n                            (first, second) =>\n                                first.timings.endDate.getTime() - second.timings.endDate.getTime(),\n                        ),\n                ),\n                switchMap((events) =>\n                    this.isSuccess(events)\n                        ? this.emitWithScheduler(Rx.of(events))\n                        : this.emitWithScheduler(Rx.throwError(() => events)),\n                ),\n                take(1),\n            ),\n        );\n    }\n\n    private emitWithScheduler<O>(input: Rx.Observable<O>): Rx.Observable<O> {\n        return this.scheduler ? input.pipe(Rx.observeOn(this.scheduler)) : input;\n    }\n}\n"
  },
  {
    "path": "lib/concurrently.spec.ts",
    "content": "import type { CpuInfo } from 'node:os';\nimport os from 'node:os';\nimport { Writable } from 'node:stream';\n\nimport { beforeEach, expect, it, Mock, MockedObject, vi } from 'vitest';\n\nimport { createMockInstance } from './__fixtures__/create-mock-instance.js';\nimport { createFakeProcess, FakeCommand } from './__fixtures__/fake-command.js';\nimport { ChildProcess, KillProcess, SpawnCommand } from './command.js';\nimport { concurrently, ConcurrentlyCommandInput, ConcurrentlyOptions } from './concurrently.js';\nimport { FlowController } from './flow-control/flow-controller.js';\nimport { Logger } from './logger.js';\n\nlet spawn: SpawnCommand;\nlet kill: KillProcess;\nlet onFinishHooks: Mock[];\nlet controllers: MockedObject<FlowController>[];\nlet processes: ChildProcess[];\nconst create = (commands: ConcurrentlyCommandInput[], options: Partial<ConcurrentlyOptions> = {}) =>\n    concurrently(commands, Object.assign(options, { controllers, spawn, kill }));\n\nbeforeEach(() => {\n    vi.resetAllMocks();\n\n    processes = [];\n    spawn = vi.fn(() => {\n        const process = createFakeProcess(processes.length);\n        processes.push(process);\n        return process;\n    });\n    kill = vi.fn();\n\n    onFinishHooks = [vi.fn(), vi.fn()];\n    controllers = [\n        { handle: vi.fn((commands) => ({ commands, onFinish: onFinishHooks[0] })) },\n        { handle: vi.fn((commands) => ({ commands, onFinish: onFinishHooks[1] })) },\n    ];\n});\n\nit('fails if commands is not an array', () => {\n    const bomb = () => create('foo' as never);\n    expect(bomb).toThrow();\n});\n\nit('fails if no commands were provided', () => {\n    const bomb = () => create([]);\n    expect(bomb).toThrow();\n});\n\nit('spawns all commands', () => {\n    create(['echo', 'kill']);\n    expect(spawn).toHaveBeenCalledTimes(2);\n    expect(spawn).toHaveBeenCalledWith('echo', expect.objectContaining({}));\n    expect(spawn).toHaveBeenCalledWith('kill', expect.objectContaining({}));\n});\n\nit('log output is passed to output stream if logger is specified in options', () => {\n    const logger = new Logger({ hide: [] });\n    const outputStream = createMockInstance(Writable);\n    create(['foo'], { logger, outputStream });\n    logger.log('foo', 'bar');\n\n    expect(outputStream.write).toHaveBeenCalledTimes(2);\n    expect(outputStream.write).toHaveBeenCalledWith('foo');\n    expect(outputStream.write).toHaveBeenCalledWith('bar');\n});\n\nit('log output is not passed to output stream after it has errored', () => {\n    const logger = new Logger({ hide: [] });\n    const outputStream = new Writable();\n    vi.spyOn(outputStream, 'write');\n\n    create(['foo'], { logger, outputStream });\n    outputStream.emit('error', new Error('test'));\n    logger.log('foo', 'bar');\n\n    expect(outputStream.write).not.toHaveBeenCalled();\n});\n\nit('spawns commands up to configured limit at once', () => {\n    create(['foo', 'bar', 'baz', 'qux'], { maxProcesses: 2 });\n    expect(spawn).toHaveBeenCalledTimes(2);\n    expect(spawn).toHaveBeenCalledWith('foo', expect.objectContaining({}));\n    expect(spawn).toHaveBeenCalledWith('bar', expect.objectContaining({}));\n\n    // Test out of order completion picking up new processes in-order\n    processes[1].emit('close', 1, null);\n    expect(spawn).toHaveBeenCalledTimes(3);\n    expect(spawn).toHaveBeenCalledWith('baz', expect.objectContaining({}));\n\n    processes[0].emit('close', null, 'SIGINT');\n    expect(spawn).toHaveBeenCalledTimes(4);\n    expect(spawn).toHaveBeenCalledWith('qux', expect.objectContaining({}));\n\n    // Shouldn't attempt to spawn anything else.\n    processes[2].emit('close', 1, null);\n    expect(spawn).toHaveBeenCalledTimes(4);\n});\n\nit('spawns commands up to percent based limit at once', () => {\n    // Mock architecture with 4 cores\n    const cpusSpy = vi.spyOn(os, 'cpus');\n    cpusSpy.mockReturnValue(\n        Array.from<CpuInfo>({ length: 4 }).fill({\n            model: 'Intel',\n            speed: 0,\n            times: { user: 0, nice: 0, sys: 0, idle: 0, irq: 0 },\n        }),\n    );\n\n    create(['foo', 'bar', 'baz', 'qux'], { maxProcesses: '50%' });\n\n    // Max parallel processes should be 2 (50% of 4 cores)\n    expect(spawn).toHaveBeenCalledTimes(2);\n    expect(spawn).toHaveBeenCalledWith('foo', expect.objectContaining({}));\n    expect(spawn).toHaveBeenCalledWith('bar', expect.objectContaining({}));\n\n    // Close first process and expect third to be spawned\n    processes[0].emit('close', 1, null);\n    expect(spawn).toHaveBeenCalledTimes(3);\n    expect(spawn).toHaveBeenCalledWith('baz', expect.objectContaining({}));\n\n    // Close second process and expect fourth to be spawned\n    processes[1].emit('close', 1, null);\n    expect(spawn).toHaveBeenCalledTimes(4);\n    expect(spawn).toHaveBeenCalledWith('qux', expect.objectContaining({}));\n});\n\nit('does not spawn further commands on abort signal aborted', () => {\n    const abortController = new AbortController();\n    create(['foo', 'bar'], { maxProcesses: 1, abortSignal: abortController.signal });\n    expect(spawn).toHaveBeenCalledTimes(1);\n\n    abortController.abort();\n    processes[0].emit('close', 0, null);\n    expect(spawn).toHaveBeenCalledTimes(1);\n});\n\nit('runs controllers with the commands', () => {\n    create(['echo', '\"echo wrapped\"']);\n\n    controllers.forEach((controller) => {\n        expect(controller.handle).toHaveBeenCalledWith([\n            expect.objectContaining({ command: 'echo', index: 0 }),\n            expect.objectContaining({ command: 'echo wrapped', index: 1 }),\n        ]);\n    });\n});\n\nit('runs commands with a name or prefix color', () => {\n    create([{ command: 'echo', prefixColor: 'red', name: 'foo' }, 'kill']);\n\n    controllers.forEach((controller) => {\n        expect(controller.handle).toHaveBeenCalledWith([\n            expect.objectContaining({ command: 'echo', index: 0, name: 'foo', prefixColor: 'red' }),\n            expect.objectContaining({ command: 'kill', index: 1, name: '', prefixColor: '' }),\n        ]);\n    });\n});\n\nit('runs commands with a list of colors', () => {\n    create(['echo', 'kill'], {\n        prefixColors: ['red'],\n    });\n\n    controllers.forEach((controller) => {\n        expect(controller.handle).toHaveBeenCalledWith([\n            expect.objectContaining({ command: 'echo', prefixColor: 'red' }),\n            expect.objectContaining({ command: 'kill', prefixColor: 'red' }),\n        ]);\n    });\n});\n\nit('passes commands wrapped from a controller to the next one', () => {\n    const fakeCommand = new FakeCommand('banana', 'banana');\n    controllers[0].handle.mockReturnValue({ commands: [fakeCommand] });\n\n    create(['echo']);\n\n    expect(controllers[0].handle).toHaveBeenCalledWith([\n        expect.objectContaining({ command: 'echo', index: 0 }),\n    ]);\n\n    expect(controllers[1].handle).toHaveBeenCalledWith([fakeCommand]);\n\n    expect(fakeCommand.start).toHaveBeenCalledTimes(1);\n});\n\nit('merges extra env vars into each command', () => {\n    create([\n        { command: 'echo', env: { foo: 'bar' } },\n        { command: 'echo', env: { foo: 'baz' } },\n        'kill',\n    ]);\n\n    expect(spawn).toHaveBeenCalledTimes(3);\n    expect(spawn).toHaveBeenCalledWith(\n        'echo',\n        expect.objectContaining({\n            env: expect.objectContaining({ foo: 'bar' }),\n        }),\n    );\n    expect(spawn).toHaveBeenCalledWith(\n        'echo',\n        expect.objectContaining({\n            env: expect.objectContaining({ foo: 'baz' }),\n        }),\n    );\n    expect(spawn).toHaveBeenCalledWith(\n        'kill',\n        expect.objectContaining({\n            env: expect.not.objectContaining({ foo: expect.anything() }),\n        }),\n    );\n});\n\nit('uses cwd from options for each command', () => {\n    create(\n        [\n            { command: 'echo', env: { foo: 'bar' } },\n            { command: 'echo', env: { foo: 'baz' } },\n            'kill',\n        ],\n        {\n            cwd: 'foobar',\n        },\n    );\n\n    expect(spawn).toHaveBeenCalledTimes(3);\n    expect(spawn).toHaveBeenCalledWith(\n        'echo',\n        expect.objectContaining({\n            env: expect.objectContaining({ foo: 'bar' }),\n            cwd: 'foobar',\n        }),\n    );\n    expect(spawn).toHaveBeenCalledWith(\n        'echo',\n        expect.objectContaining({\n            env: expect.objectContaining({ foo: 'baz' }),\n            cwd: 'foobar',\n        }),\n    );\n    expect(spawn).toHaveBeenCalledWith(\n        'kill',\n        expect.objectContaining({\n            env: expect.not.objectContaining({ foo: expect.anything() }),\n            cwd: 'foobar',\n        }),\n    );\n});\n\nit('uses overridden cwd option for each command if specified', () => {\n    create(\n        [\n            { command: 'echo', env: { foo: 'bar' }, cwd: 'baz' },\n            { command: 'echo', env: { foo: 'baz' } },\n        ],\n        {\n            cwd: 'foobar',\n        },\n    );\n\n    expect(spawn).toHaveBeenCalledTimes(2);\n    expect(spawn).toHaveBeenCalledWith(\n        'echo',\n        expect.objectContaining({\n            env: expect.objectContaining({ foo: 'bar' }),\n            cwd: 'baz',\n        }),\n    );\n    expect(spawn).toHaveBeenCalledWith(\n        'echo',\n        expect.objectContaining({\n            env: expect.objectContaining({ foo: 'baz' }),\n            cwd: 'foobar',\n        }),\n    );\n});\n\nit('uses raw from options for each command', () => {\n    create([{ command: 'echo' }, 'kill'], {\n        raw: true,\n    });\n\n    expect(spawn).toHaveBeenCalledTimes(2);\n    expect(spawn).toHaveBeenCalledWith(\n        'echo',\n        expect.objectContaining({\n            stdio: ['inherit', 'inherit', 'inherit'],\n        }),\n    );\n    expect(spawn).toHaveBeenCalledWith(\n        'kill',\n        expect.objectContaining({\n            stdio: ['inherit', 'inherit', 'inherit'],\n        }),\n    );\n});\n\nit('uses overridden raw option for each command if specified', () => {\n    create([{ command: 'echo', raw: false }, { command: 'echo' }], {\n        raw: true,\n    });\n\n    expect(spawn).toHaveBeenCalledTimes(2);\n    expect(spawn).toHaveBeenCalledWith(\n        'echo',\n        expect.objectContaining({\n            stdio: ['pipe', 'pipe', 'pipe'],\n        }),\n    );\n    expect(spawn).toHaveBeenCalledWith(\n        'echo',\n        expect.objectContaining({\n            stdio: ['inherit', 'inherit', 'inherit'],\n        }),\n    );\n});\n\nit('uses hide from options for each command', () => {\n    create([{ command: 'echo' }, 'kill'], {\n        hide: [1],\n    });\n\n    expect(spawn).toHaveBeenCalledTimes(2);\n    expect(spawn).toHaveBeenCalledWith(\n        'echo',\n        expect.objectContaining({\n            stdio: ['pipe', 'pipe', 'pipe'],\n        }),\n    );\n    expect(spawn).toHaveBeenCalledWith(\n        'kill',\n        expect.objectContaining({\n            stdio: ['pipe', 'ignore', 'ignore'],\n        }),\n    );\n});\n\nit('hides output for commands even if raw option is on', () => {\n    create([{ command: 'echo' }, 'kill'], {\n        hide: [1],\n        raw: true,\n    });\n\n    expect(spawn).toHaveBeenCalledTimes(2);\n    expect(spawn).toHaveBeenCalledWith(\n        'echo',\n        expect.objectContaining({\n            stdio: ['inherit', 'inherit', 'inherit'],\n        }),\n    );\n    expect(spawn).toHaveBeenCalledWith(\n        'kill',\n        expect.objectContaining({\n            stdio: ['pipe', 'ignore', 'ignore'],\n        }),\n    );\n});\n\nit('argument placeholders are properly replaced when additional arguments are passed', () => {\n    create(\n        [\n            { command: 'echo {1}' },\n            { command: 'echo {@}' },\n            { command: 'echo {*}' },\n            { command: 'echo \\\\{@}' },\n        ],\n        {\n            additionalArguments: ['foo', 'bar'],\n        },\n    );\n\n    expect(spawn).toHaveBeenCalledTimes(4);\n    expect(spawn).toHaveBeenCalledWith('echo foo', expect.objectContaining({}));\n    expect(spawn).toHaveBeenCalledWith('echo foo bar', expect.objectContaining({}));\n    expect(spawn).toHaveBeenCalledWith(\"echo 'foo bar'\", expect.objectContaining({}));\n    expect(spawn).toHaveBeenCalledWith('echo {@}', expect.objectContaining({}));\n});\n\nit('argument placeholders are not replaced when additional arguments are not defined', () => {\n    create([\n        { command: 'echo {1}' },\n        { command: 'echo {@}' },\n        { command: 'echo {*}' },\n        { command: 'echo \\\\{@}' },\n    ]);\n\n    expect(spawn).toHaveBeenCalledTimes(4);\n    expect(spawn).toHaveBeenCalledWith('echo {1}', expect.objectContaining({}));\n    expect(spawn).toHaveBeenCalledWith('echo {@}', expect.objectContaining({}));\n    expect(spawn).toHaveBeenCalledWith('echo {*}', expect.objectContaining({}));\n    expect(spawn).toHaveBeenCalledWith('echo {@}', expect.objectContaining({}));\n});\n\nit('runs onFinish hook after all commands run', async () => {\n    const promise = create(['foo', 'bar'], { maxProcesses: 1 });\n    expect(spawn).toHaveBeenCalledTimes(1);\n    expect(onFinishHooks[0]).not.toHaveBeenCalled();\n    expect(onFinishHooks[1]).not.toHaveBeenCalled();\n\n    processes[0].emit('close', 0, null);\n    expect(spawn).toHaveBeenCalledTimes(2);\n    expect(onFinishHooks[0]).not.toHaveBeenCalled();\n    expect(onFinishHooks[1]).not.toHaveBeenCalled();\n\n    processes[1].emit('close', 0, null);\n    await promise.result;\n\n    expect(onFinishHooks[0]).toHaveBeenCalled();\n    expect(onFinishHooks[1]).toHaveBeenCalled();\n});\n\n// This test should time out if broken\nit('waits for onFinish hooks to complete before resolving', async () => {\n    onFinishHooks[0].mockResolvedValue(undefined);\n    const { result } = create(['foo', 'bar']);\n\n    processes[0].emit('close', 0, null);\n    processes[1].emit('close', 0, null);\n\n    await expect(result).resolves.toBeDefined();\n});\n\nit('rejects if onFinish hooks reject', async () => {\n    onFinishHooks[0].mockRejectedValue('error');\n    const { result } = create(['foo', 'bar']);\n\n    processes[0].emit('close', 0, null);\n    processes[1].emit('close', 0, null);\n\n    await expect(result).rejects.toBe('error');\n});\n"
  },
  {
    "path": "lib/concurrently.ts",
    "content": "import assert from 'node:assert';\nimport os from 'node:os';\nimport { Writable } from 'node:stream';\n\nimport { takeUntil } from 'rxjs';\nimport treeKill from 'tree-kill';\n\nimport {\n    CloseEvent,\n    Command,\n    CommandIdentifier,\n    CommandInfo,\n    KillProcess,\n    SpawnCommand,\n} from './command.js';\nimport { CommandParser } from './command-parser/command-parser.js';\nimport { ExpandArguments } from './command-parser/expand-arguments.js';\nimport { ExpandShortcut } from './command-parser/expand-shortcut.js';\nimport { ExpandWildcard } from './command-parser/expand-wildcard.js';\nimport { StripQuotes } from './command-parser/strip-quotes.js';\nimport { CompletionListener, SuccessCondition } from './completion-listener.js';\nimport { FlowController } from './flow-control/flow-controller.js';\nimport { Logger } from './logger.js';\nimport { OutputWriter } from './output-writer.js';\nimport { PrefixColorSelector } from './prefix-color-selector.js';\nimport { getSpawnOpts, spawn } from './spawn.js';\nimport { castArray } from './utils.js';\n\nconst defaults: ConcurrentlyOptions = {\n    spawn,\n    kill: treeKill,\n    raw: false,\n    controllers: [],\n    cwd: undefined,\n};\n\n/**\n * A command that is to be passed into `concurrently()`.\n * If value is a string, then that's the command's command line.\n * Fine grained options can be defined by using the object format.\n */\nexport type ConcurrentlyCommandInput = string | ({ command: string } & Partial<CommandInfo>);\n\nexport interface ConcurrentlyResult {\n    /**\n     * All commands created and ran by concurrently.\n     */\n    commands: Command[];\n\n    /**\n     * A promise that resolves when concurrently ran successfully according to the specified\n     * success condition, or reject otherwise.\n     *\n     * Both the resolved and rejected value is a list of all the close events for commands that\n     * spawned; commands that didn't spawn are filtered out.\n     */\n    result: Promise<CloseEvent[]>;\n}\n\nexport interface ConcurrentlyOptions {\n    logger?: Logger;\n\n    /**\n     * Which stream should the commands output be written to.\n     */\n    outputStream?: Writable;\n\n    /**\n     * Whether the output should be ordered as if the commands were run sequentially.\n     */\n    group?: boolean;\n\n    /**\n     * A comma-separated list of Chalk colors or a string for available styles listed below to use on prefixes.\n     * If there are more commands than colors, the last color will be repeated.\n     *\n     * Available modifiers:\n     * - `reset`, `bold`, `dim`, `italic`, `underline`, `inverse`, `hidden`, `strikethrough`\n     *\n     * Available colors:\n     * - `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     *\n     * Available background colors:\n     * - `bgBlack`, `bgRed`, `bgGreen`, `bgYellow`, `bgBlue`, `bgMagenta`, `bgCyan`, `bgWhite`\n     *\n     * Set to `false` to disable colors.\n     *\n     * @see {@link https://www.npmjs.com/package/chalk} for more information.\n     */\n    prefixColors?: string | string[] | false;\n\n    /**\n     * Maximum number of commands to run at once.\n     * Exact number or a percent of CPUs available (for example \"50%\").\n     *\n     * If undefined, then all processes will start in parallel.\n     * Setting this value to 1 will achieve sequential running.\n     */\n    maxProcesses?: number | string;\n\n    /**\n     * Whether commands should be spawned in raw mode.\n     * Defaults to false.\n     */\n    raw?: boolean;\n\n    /**\n     * Which commands should have their output hidden.\n     */\n    hide?: CommandIdentifier[];\n\n    /**\n     * The current working directory of commands which didn't specify one.\n     * Defaults to `process.cwd()`.\n     */\n    cwd?: string;\n\n    /**\n     * @see CompletionListener\n     */\n    successCondition?: SuccessCondition;\n\n    /**\n     * A signal to stop spawning further processes.\n     */\n    abortSignal?: AbortSignal;\n\n    /**\n     * Which flow controllers should be applied on commands spawned by concurrently.\n     * Defaults to an empty array.\n     */\n    controllers: FlowController[];\n\n    /**\n     * A function that will spawn commands.\n     * Defaults to a function that spawns using either `cmd.exe` or `/bin/sh`.\n     */\n    spawn: SpawnCommand;\n\n    /**\n     * A function that will kill processes.\n     * Defaults to the `tree-kill` module.\n     */\n    kill: KillProcess;\n\n    /**\n     * List of additional arguments passed that will get replaced in each command.\n     * If not defined, no argument replacing will happen.\n     *\n     * @see ExpandArguments\n     */\n    additionalArguments?: string[];\n}\n\n/**\n * Core concurrently functionality -- spawns the given commands concurrently and\n * returns the commands themselves + the result according to the specified success condition.\n *\n * @see CompletionListener\n */\nexport function concurrently(\n    baseCommands: ConcurrentlyCommandInput[],\n    baseOptions?: Partial<ConcurrentlyOptions>,\n): ConcurrentlyResult {\n    assert.ok(Array.isArray(baseCommands), '[concurrently] commands should be an array');\n    assert.notStrictEqual(baseCommands.length, 0, '[concurrently] no commands provided');\n\n    const options = { ...defaults, ...baseOptions };\n\n    const prefixColorSelector = new PrefixColorSelector(options.prefixColors || []);\n\n    const commandParsers: CommandParser[] = [\n        new StripQuotes(),\n        new ExpandShortcut(),\n        new ExpandWildcard(),\n    ];\n\n    if (options.additionalArguments) {\n        commandParsers.push(new ExpandArguments(options.additionalArguments));\n    }\n\n    const hide = (options.hide || []).map(String);\n    let commands = baseCommands\n        .map(mapToCommandInfo)\n        .flatMap((command) => parseCommand(command, commandParsers))\n        .map((command, index) => {\n            const hidden = hide.includes(command.name) || hide.includes(String(index));\n            return new Command(\n                {\n                    index,\n                    prefixColor: prefixColorSelector.getNextColor(),\n                    ...command,\n                },\n                getSpawnOpts({\n                    ipc: command.ipc,\n                    stdio: hidden ? 'hidden' : (command.raw ?? options.raw) ? 'raw' : 'normal',\n                    env: command.env,\n                    cwd: command.cwd || options.cwd,\n                }),\n                options.spawn,\n                options.kill,\n            );\n        });\n\n    const handleResult = options.controllers.reduce(\n        ({ commands: prevCommands, onFinishCallbacks }, controller) => {\n            const { commands, onFinish } = controller.handle(prevCommands);\n            return {\n                commands,\n                onFinishCallbacks: onFinishCallbacks.concat(onFinish ? [onFinish] : []),\n            };\n        },\n        { commands, onFinishCallbacks: [] } as {\n            commands: Command[];\n            onFinishCallbacks: (() => void)[];\n        },\n    );\n    commands = handleResult.commands;\n\n    if (options.logger && options.outputStream) {\n        const outputWriter = new OutputWriter({\n            outputStream: options.outputStream,\n            group: !!options.group,\n            commands,\n        });\n        options.logger.output\n            // Stop trying to write after there's been an error.\n            .pipe(takeUntil(outputWriter.error))\n            .subscribe(({ command, text }) => outputWriter.write(command, text));\n    }\n\n    const commandsLeft = commands.slice();\n    const maxProcesses = Math.max(\n        1,\n        (typeof options.maxProcesses === 'string' && options.maxProcesses.endsWith('%')\n            ? Math.round((os.cpus().length * Number(options.maxProcesses.slice(0, -1))) / 100)\n            : Number(options.maxProcesses)) || commandsLeft.length,\n    );\n    for (let i = 0; i < maxProcesses; i++) {\n        maybeRunMore(commandsLeft, options.abortSignal);\n    }\n\n    const result = new CompletionListener({ successCondition: options.successCondition })\n        .listen(commands, options.abortSignal)\n        .finally(() => Promise.all(handleResult.onFinishCallbacks.map((onFinish) => onFinish())));\n\n    return {\n        result,\n        commands,\n    };\n}\n\nfunction mapToCommandInfo(command: ConcurrentlyCommandInput): CommandInfo {\n    if (typeof command === 'string') {\n        return mapToCommandInfo({ command });\n    }\n\n    assert.ok(command.command, '[concurrently] command cannot be empty');\n    return {\n        command: command.command,\n        name: command.name || '',\n        env: command.env || {},\n        cwd: command.cwd || '',\n        ipc: command.ipc,\n        ...(command.prefixColor\n            ? {\n                  prefixColor: command.prefixColor,\n              }\n            : {}),\n        ...(command.raw !== undefined\n            ? {\n                  raw: command.raw,\n              }\n            : {}),\n    };\n}\n\nfunction parseCommand(command: CommandInfo, parsers: CommandParser[]) {\n    return parsers.reduce(\n        (commands, parser) => commands.flatMap((command) => parser.parse(command)),\n        castArray(command),\n    );\n}\n\nfunction maybeRunMore(commandsLeft: Command[], abortSignal?: AbortSignal) {\n    const command = commandsLeft.shift();\n    if (!command || abortSignal?.aborted) {\n        return;\n    }\n\n    command.start();\n    command.close.subscribe(() => {\n        maybeRunMore(commandsLeft, abortSignal);\n    });\n}\n"
  },
  {
    "path": "lib/date-format.spec.ts",
    "content": "import { afterAll, beforeAll, describe, expect, it } from 'vitest';\n\nimport { DateFormatter, FormatterOptions } from './date-format.js';\n\nconst withTime = (time: string) => `2000-01-01T${time}`;\nconst withDate = (date: string) => `${date}T00:00:00`;\n\ntype TokenTests = undefined | { input: string; expected: string }[];\n\n/**\n * Generates a suite of tests for token `token`.\n *\n * Each entry in `patternTests` makes the token longer, e.g.\n * ```\n * makeTests('year', 'y', [\n *     [{ expected: '2', input: withDate('0002-01-01') }], // y\n *     [{ expected: '02', input: withDate('0002-01-01') }], // yy\n *     // ...\n * ]);\n * ```\n */\nconst makeTests = (\n    name: string,\n    token: string,\n    patternTests: TokenTests[],\n    options?: FormatterOptions,\n) =>\n    describe(`${name}`, () => {\n        patternTests.forEach((tests, i) => {\n            const pattern = token.repeat(i + 1);\n            if (!tests) {\n                return it(`is not implemented for ${pattern}`, () => {\n                    expect(() => new DateFormatter(pattern)).toThrow(RangeError);\n                });\n            } else if (!tests.length) {\n                return;\n            }\n\n            it.each(tests)(\n                `for pattern ${pattern} and input \"$input\" returns \"$expected\"`,\n                ({ expected, input }) => {\n                    const formatter = new DateFormatter(pattern, {\n                        locale: 'en',\n                        calendar: 'gregory',\n                        ...options,\n                    });\n                    expect(formatter.format(new Date(input))).toBe(expected);\n                },\n            );\n        });\n    });\n\ndescribe('combined', () => {\n    it('works with tokens and punctuation', () => {\n        const formatter = new DateFormatter('yyyy-MM-dd HH:mm:ss', { locale: 'en' });\n        const date = new Date(2024, 8, 1, 15, 30, 50);\n        expect(formatter.format(date)).toBe('2024-09-01 15:30:50');\n    });\n\n    it('works with tokens and literals', () => {\n        const formatter = new DateFormatter(\"HH 'o''clock'\", { locale: 'en' });\n        const date = new Date();\n        date.setHours(10);\n        expect(formatter.format(date)).toBe(\"10 o'clock\");\n    });\n});\n\ndescribe('literals', () => {\n    it.each([\n        [\"'\", \"''\"],\n        ['foo bar', \"'foo bar'\"],\n        [\"foo ' bar\", \"'foo '' bar'\"],\n        [\"foo bar'?\", \"'foo bar'''?\"],\n    ])('returns \"%s\" for pattern \"%s\"', (expected, pattern) => {\n        const formatter = new DateFormatter(pattern);\n        expect(formatter.format(new Date())).toBe(expected);\n    });\n});\n\ndescribe('tokens', () => {\n    it('throws if a token does not exist', () => {\n        expect(() => new DateFormatter('t')).toThrow(SyntaxError);\n    });\n\n    makeTests('era', 'G', [\n        [\n            { input: withDate('-000001-01-01'), expected: 'BC' },\n            { input: withDate('0001-01-01'), expected: 'AD' },\n        ],\n        [\n            { input: withDate('-000001-01-01'), expected: 'BC' },\n            { input: withDate('0001-01-01'), expected: 'AD' },\n        ],\n        [\n            { input: withDate('-000001-01-01'), expected: 'BC' },\n            { input: withDate('0001-01-01'), expected: 'AD' },\n        ],\n        [\n            { input: withDate('-000001-01-01'), expected: 'Before Christ' },\n            { input: withDate('0001-01-01'), expected: 'Anno Domini' },\n        ],\n        [\n            { input: withDate('-000001-01-01'), expected: 'B' },\n            { input: withDate('0001-01-01'), expected: 'A' },\n        ],\n    ]);\n\n    makeTests('year', 'y', [\n        [\n            { expected: '2', input: withDate('0002-01-01') },\n            { expected: '20', input: withDate('0020-01-01') },\n            { expected: '200', input: withDate('0200-01-01') },\n            { expected: '2000', input: withDate('2000-01-01') },\n        ],\n        [\n            { expected: '02', input: withDate('0002-01-01') },\n            { expected: '20', input: withDate('0020-01-01') },\n            { expected: '00', input: withDate('0200-01-01') },\n            { expected: '05', input: withDate('0205-01-01') },\n            { expected: '00', input: withDate('2000-01-01') },\n            { expected: '05', input: withDate('2005-01-01') },\n        ],\n        [\n            { expected: '002', input: withDate('0002-01-01') },\n            { expected: '020', input: withDate('0020-01-01') },\n            { expected: '200', input: withDate('0200-01-01') },\n            { expected: '2000', input: withDate('2000-01-01') },\n        ],\n        [\n            { expected: '0002', input: withDate('0002-01-01') },\n            { expected: '0020', input: withDate('0020-01-01') },\n            { expected: '0200', input: withDate('0200-01-01') },\n            { expected: '2000', input: withDate('2000-01-01') },\n        ],\n        [\n            { expected: '00002', input: withDate('0002-01-01') },\n            { expected: '00020', input: withDate('0020-01-01') },\n            { expected: '00200', input: withDate('0200-01-01') },\n            { expected: '02000', input: withDate('2000-01-01') },\n        ],\n    ]);\n\n    describe('year name', () => {\n        makeTests('with en locale', 'U', [[{ expected: '2024', input: withDate('2024-01-01') }]], {\n            locale: 'en',\n        });\n\n        makeTests(\n            'with zh-CN locale + chinese calendar',\n            'U',\n            [[{ expected: '癸卯', input: withDate('2024-01-01') }]],\n            { locale: 'zh-CN', calendar: 'chinese' },\n        );\n    });\n\n    describe('related year', () => {\n        makeTests('with en locale', 'r', [[{ expected: '2024', input: withDate('2024-01-01') }]], {\n            locale: 'en',\n        });\n\n        makeTests(\n            'with zh-CN locale + chinese calendar',\n            'r',\n            [\n                [\n                    { expected: '2023', input: withDate('2024-01-01') },\n                    { expected: '2024', input: withDate('2024-03-01') },\n                ],\n            ],\n            { locale: 'zh-CN', calendar: 'chinese' },\n        );\n    });\n\n    describe('week year', () => {\n        makeTests(\n            'with en locale',\n            'Y',\n            [\n                [\n                    { expected: '2023', input: withDate('2023-01-01') },\n                    { expected: '2024', input: withDate('2023-12-31') },\n                    { expected: '2024', input: withDate('2024-01-01') },\n                    { expected: '2025', input: withDate('2024-12-31') },\n                ],\n                [\n                    { expected: '23', input: withDate('2023-01-01') },\n                    { expected: '24', input: withDate('2023-12-31') },\n                    { expected: '24', input: withDate('2024-01-01') },\n                    { expected: '25', input: withDate('2024-12-31') },\n                ],\n            ],\n            { locale: 'en' },\n        );\n\n        makeTests(\n            'with de-DE locale',\n            'Y',\n            [\n                [\n                    { expected: '2022', input: withDate('2023-01-01') },\n                    { expected: '2023', input: withDate('2023-12-31') },\n                    { expected: '2024', input: withDate('2024-01-01') },\n                    { expected: '2025', input: withDate('2024-12-31') },\n                ],\n                [\n                    { expected: '22', input: withDate('2023-01-01') },\n                    { expected: '23', input: withDate('2023-12-31') },\n                    { expected: '24', input: withDate('2024-01-01') },\n                    { expected: '25', input: withDate('2024-12-31') },\n                ],\n            ],\n            { locale: 'de-DE' },\n        );\n\n        describe(`when minimalDays is missing`, () => {\n            beforeAll(() => {\n                if (typeof Intl.Locale.prototype.getWeekInfo === 'function') {\n                    Intl.Locale.prototype.getWeekInfoOrig = Intl.Locale.prototype.getWeekInfo;\n                }\n\n                Intl.Locale.prototype.getWeekInfo = function () {\n                    const data =\n                        typeof Intl.Locale.prototype.getWeekInfoOrig === 'function'\n                            ? this.getWeekInfoOrig()\n                            : this.weekInfo;\n                    delete data.minimalDays;\n                    return data;\n                };\n            });\n\n            afterAll(() => {\n                if (Intl.Locale.prototype.getWeekInfoOrig) {\n                    Intl.Locale.prototype.getWeekInfo = Intl.Locale.prototype.getWeekInfoOrig;\n                    delete Intl.Locale.prototype.getWeekInfoOrig;\n                }\n            });\n\n            makeTests(\n                // Needs to be a different locale than in tests above to not use cached weekInfo\n                'with de-CH locale',\n                'Y',\n                [\n                    [\n                        { expected: '2022', input: withDate('2023-01-01') },\n                        { expected: '2023', input: withDate('2023-12-31') },\n                        { expected: '2024', input: withDate('2024-01-01') },\n                        { expected: '2025', input: withDate('2024-12-31') },\n                    ],\n                    [\n                        { expected: '22', input: withDate('2023-01-01') },\n                        { expected: '23', input: withDate('2023-12-31') },\n                        { expected: '24', input: withDate('2024-01-01') },\n                        { expected: '25', input: withDate('2024-12-31') },\n                    ],\n                ],\n                { locale: 'de-CH' },\n            );\n        });\n    });\n\n    makeTests('quarter', 'Q', [\n        [\n            { expected: '1', input: withDate('2000-01-01') },\n            { expected: '2', input: withDate('2000-04-01') },\n            { expected: '3', input: withDate('2000-07-01') },\n            { expected: '4', input: withDate('2000-10-01') },\n        ],\n        [\n            { expected: '01', input: withDate('2000-01-01') },\n            { expected: '02', input: withDate('2000-04-01') },\n            { expected: '03', input: withDate('2000-07-01') },\n            { expected: '04', input: withDate('2000-10-01') },\n        ],\n        undefined,\n        undefined,\n        [\n            { expected: '1', input: withDate('2000-01-01') },\n            { expected: '2', input: withDate('2000-04-01') },\n            { expected: '3', input: withDate('2000-07-01') },\n            { expected: '4', input: withDate('2000-10-01') },\n        ],\n    ]);\n\n    makeTests('quarter - stand-alone', 'q', [\n        [\n            { expected: '1', input: withDate('2000-01-01') },\n            { expected: '2', input: withDate('2000-04-01') },\n            { expected: '3', input: withDate('2000-07-01') },\n            { expected: '4', input: withDate('2000-10-01') },\n        ],\n        [\n            { expected: '01', input: withDate('2000-01-01') },\n            { expected: '02', input: withDate('2000-04-01') },\n            { expected: '03', input: withDate('2000-07-01') },\n            { expected: '04', input: withDate('2000-10-01') },\n        ],\n        undefined,\n        undefined,\n        [\n            { expected: '1', input: withDate('2000-01-01') },\n            { expected: '2', input: withDate('2000-04-01') },\n            { expected: '3', input: withDate('2000-07-01') },\n            { expected: '4', input: withDate('2000-10-01') },\n        ],\n    ]);\n\n    describe('month', () => {\n        makeTests(\n            'with en locale',\n            'M',\n            [\n                [{ expected: '1', input: withDate('2000-01-01') }],\n                [{ expected: '01', input: withDate('2000-01-01') }],\n                [{ expected: 'Jan', input: withDate('2000-01-01') }],\n                [{ expected: 'January', input: withDate('2000-01-01') }],\n                [{ expected: 'J', input: withDate('2000-01-01') }],\n            ],\n            { locale: 'en' },\n        );\n\n        makeTests(\n            'with pl locale',\n            'M',\n            [\n                [{ expected: '1', input: withDate('2000-01-01') }],\n                [{ expected: '01', input: withDate('2000-01-01') }],\n                [{ expected: 'sty', input: withDate('2000-01-01') }],\n                [{ expected: 'stycznia', input: withDate('2000-01-01') }],\n                [{ expected: 's', input: withDate('2000-01-01') }],\n            ],\n            { locale: 'pl' },\n        );\n    });\n\n    describe('month - stand-alone', () => {\n        makeTests(\n            'with en locale',\n            'L',\n            [\n                [{ expected: '1', input: withDate('2000-01-01') }],\n                [{ expected: '01', input: withDate('2000-01-01') }],\n                [{ expected: 'Jan', input: withDate('2000-01-01') }],\n                [{ expected: 'January', input: withDate('2000-01-01') }],\n                [{ expected: 'J', input: withDate('2000-01-01') }],\n            ],\n            { locale: 'en' },\n        );\n\n        makeTests(\n            'with pl locale',\n            'L',\n            [\n                [{ expected: '1', input: withDate('2000-01-01') }],\n                [{ expected: '01', input: withDate('2000-01-01') }],\n                [{ expected: 'sty', input: withDate('2000-01-01') }],\n                [{ expected: 'styczeń', input: withDate('2000-01-01') }],\n                [{ expected: 'S', input: withDate('2000-01-01') }],\n            ],\n            { locale: 'pl' },\n        );\n    });\n\n    describe('week of year', () => {\n        makeTests(\n            'with en locale',\n            'w',\n            [\n                [\n                    { expected: '1', input: withDate('2023-01-01') },\n                    { expected: '1', input: withDate('2023-12-31') },\n                    { expected: '1', input: withDate('2024-01-01') },\n                    { expected: '1', input: withDate('2024-12-31') },\n                ],\n\n                [\n                    { expected: '01', input: withDate('2023-01-01') },\n                    { expected: '01', input: withDate('2023-12-31') },\n                    { expected: '01', input: withDate('2024-01-01') },\n                    { expected: '01', input: withDate('2024-12-31') },\n                ],\n            ],\n            { locale: 'en' },\n        );\n\n        makeTests(\n            'with de-DE locale',\n            'w',\n            [\n                [\n                    { expected: '52', input: withDate('2023-01-01') },\n                    { expected: '52', input: withDate('2023-12-31') },\n                    { expected: '1', input: withDate('2024-01-01') },\n                    { expected: '1', input: withDate('2024-12-31') },\n                ],\n\n                [\n                    { expected: '52', input: withDate('2023-01-01') },\n                    { expected: '52', input: withDate('2023-12-31') },\n                    { expected: '01', input: withDate('2024-01-01') },\n                    { expected: '01', input: withDate('2024-12-31') },\n                ],\n            ],\n            { locale: 'de-DE' },\n        );\n    });\n\n    describe('week of month', () => {\n        makeTests(\n            'with en locale',\n            'W',\n            [\n                [\n                    { expected: '6', input: withDate('2021-01-31') },\n                    { expected: '5', input: withDate('2021-02-28') },\n                ],\n            ],\n            { locale: 'en' },\n        );\n\n        makeTests(\n            'with de-DE locale',\n            'W',\n            [\n                [\n                    { expected: '5', input: withDate('2021-01-31') },\n                    { expected: '4', input: withDate('2021-02-28') },\n                ],\n            ],\n            { locale: 'de-DE' },\n        );\n    });\n\n    makeTests('day', 'd', [\n        [\n            { expected: '1', input: withDate('2000-01-01') },\n            { expected: '10', input: withDate('2000-01-10') },\n        ],\n        [\n            { expected: '01', input: withDate('2000-01-01') },\n            { expected: '10', input: withDate('2000-01-10') },\n        ],\n    ]);\n\n    makeTests('day of week in month', 'F', [\n        [\n            { expected: '1', input: withDate('2024-09-01') },\n            { expected: '2', input: withDate('2024-09-08') },\n        ],\n    ]);\n\n    makeTests('day of year', 'D', [\n        [\n            { expected: '1', input: withDate('2024-01-01') },\n            { expected: '32', input: withDate('2024-02-01') },\n            { expected: '366', input: withDate('2024-12-31') },\n        ],\n\n        [\n            { expected: '01', input: withDate('2024-01-01') },\n            { expected: '32', input: withDate('2024-02-01') },\n            { expected: '366', input: withDate('2024-12-31') },\n        ],\n\n        [\n            { expected: '001', input: withDate('2024-01-01') },\n            { expected: '032', input: withDate('2024-02-01') },\n            { expected: '366', input: withDate('2024-12-31') },\n        ],\n    ]);\n\n    makeTests('week day', 'E', [\n        [{ expected: 'Sat', input: withDate('2024-09-07') }],\n        [{ expected: 'Sat', input: withDate('2024-09-07') }],\n        [{ expected: 'Sat', input: withDate('2024-09-07') }],\n        [{ expected: 'Saturday', input: withDate('2024-09-07') }],\n    ]);\n\n    makeTests('local week day', 'e', [\n        undefined,\n        undefined,\n        [{ expected: 'Sat', input: withDate('2024-09-07') }],\n        [{ expected: 'Saturday', input: withDate('2024-09-07') }],\n    ]);\n\n    makeTests('period', 'a', [\n        [\n            { expected: 'AM', input: withTime('10:00:00') },\n            { expected: 'PM', input: withTime('12:00:00') },\n        ],\n        [\n            { expected: 'AM', input: withTime('10:00:00') },\n            { expected: 'PM', input: withTime('12:00:00') },\n        ],\n        [\n            { expected: 'AM', input: withTime('10:00:00') },\n            { expected: 'PM', input: withTime('12:00:00') },\n        ],\n    ]);\n\n    makeTests('flexible day period', 'B', [\n        [\n            { expected: 'in the morning', input: withTime('06:00:00') },\n            { expected: 'noon', input: withTime('12:00:00') },\n            { expected: 'in the afternoon', input: withTime('16:00:00') },\n            { expected: 'at night', input: withTime('23:00:00') },\n        ],\n        [],\n        [],\n        [\n            { expected: 'in the morning', input: withTime('06:00:00') },\n            { expected: 'noon', input: withTime('12:00:00') },\n            { expected: 'in the afternoon', input: withTime('16:00:00') },\n            { expected: 'at night', input: withTime('23:00:00') },\n        ],\n    ]);\n\n    describe('hour', () => {\n        makeTests('1-12 format (1 PM)', 'h', [\n            [{ expected: '1', input: withTime('13:00:00') }],\n            [{ expected: '01', input: withTime('13:00:00') }],\n        ]);\n\n        makeTests('1-12 format (12 PM)', 'h', [\n            [{ expected: '12', input: withTime('00:00:00') }],\n            [{ expected: '12', input: withTime('00:00:00') }],\n        ]);\n\n        makeTests('0-23 format', 'H', [\n            [\n                { expected: '0', input: withTime('00:00:00') },\n                { expected: '13', input: withTime('13:00:00') },\n            ],\n            [\n                { expected: '00', input: withTime('00:00:00') },\n                { expected: '13', input: withTime('13:00:00') },\n            ],\n        ]);\n\n        makeTests('0-11 format', 'K', [\n            [\n                { expected: '0', input: withTime('00:00:00') },\n                { expected: '1', input: withTime('13:00:00') },\n            ],\n            [\n                { expected: '00', input: withTime('00:00:00') },\n                { expected: '01', input: withTime('13:00:00') },\n            ],\n        ]);\n\n        makeTests('1-24 format', 'k', [\n            [\n                { expected: '13', input: withTime('13:00:00') },\n                { expected: '24', input: withTime('00:00:00') },\n            ],\n            [\n                { expected: '13', input: withTime('13:00:00') },\n                { expected: '24', input: withTime('00:00:00') },\n            ],\n        ]);\n    });\n\n    makeTests('minute', 'm', [\n        [\n            { expected: '0', input: withTime('00:00:00') },\n            { expected: '59', input: withTime('00:59:00') },\n        ],\n        [\n            { expected: '00', input: withTime('00:00:00') },\n            { expected: '59', input: withTime('00:59:00') },\n        ],\n    ]);\n\n    makeTests('seconds', 's', [\n        [\n            { expected: '0', input: withTime('00:00:00') },\n            { expected: '59', input: withTime('00:00:59') },\n        ],\n        [\n            { expected: '00', input: withTime('00:00:00') },\n            { expected: '59', input: withTime('00:00:59') },\n        ],\n    ]);\n\n    makeTests('fractional seconds', 'S', [\n        [\n            { expected: '0', input: withTime('00:00:00.000') },\n            { expected: '0', input: withTime('00:00:00.001') },\n            { expected: '0', input: withTime('00:00:00.010') },\n            { expected: '1', input: withTime('00:00:00.100') },\n        ],\n\n        [\n            { expected: '00', input: withTime('00:00:00.000') },\n            { expected: '00', input: withTime('00:00:00.001') },\n            { expected: '01', input: withTime('00:00:00.010') },\n            { expected: '10', input: withTime('00:00:00.100') },\n        ],\n\n        [\n            { expected: '000', input: withTime('00:00:00.000') },\n            { expected: '001', input: withTime('00:00:00.001') },\n            { expected: '010', input: withTime('00:00:00.010') },\n            { expected: '100', input: withTime('00:00:00.100') },\n        ],\n    ]);\n});\n"
  },
  {
    "path": "lib/date-format.ts",
    "content": "export interface FormatterOptions {\n    locale?: string;\n    calendar?: string;\n}\n\ntype TokenFormatter = (date: Date, options: FormatterOptions) => string | number;\n\n/**\n * A map of token to its implementations by length.\n * If an index is undefined, then that token length is unsupported.\n */\nconst tokens = new Map<string, (TokenFormatter | undefined)[]>()\n    // era\n    .set('G', [\n        makeTokenFn({ era: 'short' }, 'era'),\n        makeTokenFn({ era: 'short' }, 'era'),\n        makeTokenFn({ era: 'short' }, 'era'),\n        makeTokenFn({ era: 'long' }, 'era'),\n        makeTokenFn({ era: 'narrow' }, 'era'),\n    ])\n    // year\n    .set('y', [\n        // TODO: does not support BC years.\n        // https://stackoverflow.com/a/41345095/2083599\n        (date) => date.getFullYear(),\n        (date) => pad(2, date.getFullYear()).slice(-2),\n        (date) => pad(3, date.getFullYear()),\n        (date) => pad(4, date.getFullYear()),\n        (date) => pad(5, date.getFullYear()),\n    ])\n    .set('Y', [\n        getWeekYear,\n        (date, options) => pad(2, getWeekYear(date, options)).slice(-2),\n        (date, options) => pad(3, getWeekYear(date, options)),\n        (date, options) => pad(4, getWeekYear(date, options)),\n        (date, options) => pad(5, getWeekYear(date, options)),\n    ])\n    .set('u', [])\n    .set('U', [\n        // Fallback implemented as yearName is not available in gregorian calendars, for instance.\n        makeTokenFn({ dateStyle: 'full' }, 'yearName', (date) => String(date.getFullYear())),\n    ])\n    .set('r', [\n        // Fallback implemented as relatedYear is not available in gregorian calendars, for instance.\n        makeTokenFn({ dateStyle: 'full' }, 'relatedYear', (date) => String(date.getFullYear())),\n    ])\n    // quarter\n    .set('Q', [\n        (date) => Math.floor(date.getMonth() / 3) + 1,\n        (date) => pad(2, Math.floor(date.getMonth() / 3) + 1),\n        // these aren't localized in Intl.DateTimeFormat.\n        undefined,\n        undefined,\n        (date) => Math.floor(date.getMonth() / 3) + 1,\n    ])\n    .set('q', [\n        (date) => Math.floor(date.getMonth() / 3) + 1,\n        (date) => pad(2, Math.floor(date.getMonth() / 3) + 1),\n        // these aren't localized in Intl.DateTimeFormat.\n        undefined,\n        undefined,\n        (date) => Math.floor(date.getMonth() / 3) + 1,\n    ])\n    // month\n    .set('M', [\n        (date) => date.getMonth() + 1,\n        (date) => pad(2, date.getMonth() + 1),\n        // these include the day so that it forces non-stand-alone month part\n        makeTokenFn({ day: 'numeric', month: 'short' }, 'month'),\n        makeTokenFn({ day: 'numeric', month: 'long' }, 'month'),\n        makeTokenFn({ day: 'numeric', month: 'narrow' }, 'month'),\n    ])\n    .set('L', [\n        (date) => date.getMonth() + 1,\n        (date) => pad(2, date.getMonth() + 1),\n        makeTokenFn({ month: 'short' }, 'month'),\n        makeTokenFn({ month: 'long' }, 'month'),\n        makeTokenFn({ month: 'narrow' }, 'month'),\n    ])\n    .set('l', [() => ''])\n    // week\n    .set('w', [getWeek, (date, options) => pad(2, getWeek(date, options))])\n    .set('W', [getWeekOfMonth])\n    // day\n    .set('d', [(date) => date.getDate(), (date) => pad(2, date.getDate())])\n    .set('D', [\n        getDayOfYear,\n        (date) => pad(2, getDayOfYear(date)),\n        (date) => pad(3, getDayOfYear(date)),\n    ])\n    .set('F', [(date) => Math.ceil(date.getDate() / 7)])\n    .set('g', [])\n    // week day\n    .set('E', [\n        makeTokenFn({ weekday: 'short' }, 'weekday'),\n        makeTokenFn({ weekday: 'short' }, 'weekday'),\n        makeTokenFn({ weekday: 'short' }, 'weekday'),\n        makeTokenFn({ weekday: 'long' }, 'weekday'),\n    ])\n    .set('e', [\n        undefined,\n        undefined,\n        makeTokenFn({ weekday: 'short' }, 'weekday'),\n        makeTokenFn({ weekday: 'long' }, 'weekday'),\n    ])\n    .set('c', [])\n    // period\n    .set('a', [\n        makeTokenFn({ hour12: true, timeStyle: 'full' }, 'dayPeriod'),\n        makeTokenFn({ hour12: true, timeStyle: 'full' }, 'dayPeriod'),\n        makeTokenFn({ hour12: true, timeStyle: 'full' }, 'dayPeriod'),\n    ])\n    .set('b', [])\n    .set('B', [\n        makeTokenFn({ dayPeriod: 'short' }, 'dayPeriod'),\n        makeTokenFn({ dayPeriod: 'short' }, 'dayPeriod'),\n        makeTokenFn({ dayPeriod: 'short' }, 'dayPeriod'),\n        makeTokenFn({ dayPeriod: 'long' }, 'dayPeriod'),\n    ])\n    // hour\n    .set('h', [(date) => date.getHours() % 12 || 12, (date) => pad(2, date.getHours() % 12 || 12)])\n    .set('H', [(date) => date.getHours(), (date) => pad(2, date.getHours())])\n    .set('K', [(date) => date.getHours() % 12, (date) => pad(2, date.getHours() % 12)])\n    .set('k', [(date) => date.getHours() % 24 || 24, (date) => pad(2, date.getHours() % 24 || 24)])\n    .set('j', [])\n    .set('J', [])\n    .set('C', [])\n    // minute\n    .set('m', [(date) => date.getMinutes(), (date) => pad(2, date.getMinutes())])\n    // second\n    .set('s', [(date) => date.getSeconds(), (date) => pad(2, date.getSeconds())])\n    .set('S', [\n        (date) => Math.trunc(date.getMilliseconds() / 100),\n        (date) => pad(2, Math.trunc(date.getMilliseconds() / 10)),\n        (date) => pad(3, Math.trunc(date.getMilliseconds())),\n    ])\n    .set('A', [])\n    // zone\n    // none of these have tests\n    .set('z', [\n        makeTokenFn({ timeZoneName: 'short' }, 'timeZoneName'),\n        makeTokenFn({ timeZoneName: 'short' }, 'timeZoneName'),\n        makeTokenFn({ timeZoneName: 'short' }, 'timeZoneName'),\n        makeTokenFn({ timeZoneName: 'long' }, 'timeZoneName'),\n    ])\n    .set('Z', [\n        undefined,\n        undefined,\n        undefined,\n        // equivalent to `OOOO`.\n        makeTokenFn({ timeZoneName: 'longOffset' }, 'timeZoneName'),\n    ])\n    .set('O', [\n        makeTokenFn({ timeZoneName: 'shortOffset' }, 'timeZoneName'),\n        undefined,\n        undefined,\n        // equivalent to `ZZZZ`.\n        makeTokenFn({ timeZoneName: 'longOffset' }, 'timeZoneName'),\n    ])\n    .set('v', [\n        makeTokenFn({ timeZoneName: 'shortGeneric' }, 'timeZoneName'),\n        undefined,\n        undefined,\n        makeTokenFn({ timeZoneName: 'longGeneric' }, 'timeZoneName'),\n    ])\n    .set('V', [])\n    .set('X', [])\n    .set('x', []);\n\nlet locale: Intl.Locale;\nfunction getLocale(options: FormatterOptions): Intl.Locale {\n    if (!locale || locale.baseName !== options.locale) {\n        locale = new Intl.Locale(\n            /* v8 ignore next - fallback value only for safety */\n            options.locale || new Intl.DateTimeFormat().resolvedOptions().locale,\n        );\n    }\n    return locale;\n}\n\n/**\n * Unicode-compliant date/time formatter.\n *\n * @see https://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns\n */\nexport class DateFormatter {\n    private static tokenRegex = /[A-Z]/i;\n\n    private readonly parts: TokenFormatter[] = [];\n\n    constructor(\n        pattern: string,\n        private readonly options: FormatterOptions = {},\n    ) {\n        let i = 0;\n        while (i < pattern.length) {\n            const char = pattern[i];\n            const { fn, length } =\n                char === \"'\"\n                    ? this.compileLiteral(pattern, i)\n                    : DateFormatter.tokenRegex.test(char)\n                      ? this.compileToken(pattern, i)\n                      : this.compileOther(pattern, i);\n            this.parts.push(fn);\n            i += length;\n        }\n    }\n\n    private compileLiteral(pattern: string, offset: number) {\n        let length = 1;\n        let value = '';\n        for (; length < pattern.length; length++) {\n            const i = offset + length;\n            const char = pattern[i];\n\n            if (char === \"'\") {\n                const nextChar = pattern[i + 1];\n                length++;\n\n                // if the next character is another single quote, it's been escaped.\n                // if not, then the literal has been closed\n                if (nextChar !== \"'\") {\n                    break;\n                }\n            }\n\n            value += char;\n        }\n\n        return { fn: () => value || \"'\", length };\n    }\n\n    private compileOther(pattern: string, offset: number) {\n        let value = '';\n        while (!DateFormatter.tokenRegex.test(pattern[offset]) && pattern[offset] !== \"'\") {\n            value += pattern[offset++];\n        }\n\n        return { fn: () => value, length: value.length };\n    }\n\n    private compileToken(pattern: string, offset: number) {\n        const type = pattern[offset];\n        const token = tokens.get(type);\n        if (!token) {\n            throw new SyntaxError(`Formatting token \"${type}\" is invalid`);\n        }\n\n        let length = 0;\n        while (pattern[offset + length] === type) {\n            length++;\n        }\n\n        const tokenFn = token[length - 1];\n        if (!tokenFn) {\n            throw new RangeError(`Formatting token \"${type.repeat(length)}\" is unsupported`);\n        }\n\n        return { fn: tokenFn, length };\n    }\n\n    format(date: Date): string {\n        return this.parts.reduce((output, part) => output + String(part(date, this.options)), '');\n    }\n}\n\n/**\n * See https://github.com/moment/luxon/issues/1693\n *\n * @todo Replace once there's a suitable alternative implementation\n */\nconst fallbackWeekInfo = {\n    minimalDays: 4,\n};\nconst weekInfoCache = new Map<string, Intl.WeekInfo>();\nfunction getWeekInfo(locale: Intl.Locale): Intl.WeekInfo {\n    let data = weekInfoCache.get(locale.baseName);\n    if (!data) {\n        // The specs now envisage a method for this,\n        // see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/getWeekInfo\n        data = locale.getWeekInfo?.() ?? locale.weekInfo;\n        // The specs have dropped minimalDays,\n        // see https://github.com/tc39/proposal-intl-locale-info/issues/86\n        if (!data.minimalDays) {\n            data = { ...fallbackWeekInfo, ...data };\n        }\n        weekInfoCache.set(locale.baseName, data);\n    }\n    return data;\n}\n\n/**\n * Creates a token formatting function that returns the value of the chosen part type,\n * using the current locale's settings.\n *\n * If the date/formatter settings doesn't include the requested part type,\n * the `fallback` function is invoked, if specified. If none has been specified, returns an\n * empty string.\n */\nfunction makeTokenFn(\n    options: Intl.DateTimeFormatOptions,\n    type: Intl.DateTimeFormatPartTypes,\n    fallback?: TokenFormatter,\n): TokenFormatter {\n    let formatter: Intl.DateTimeFormat;\n    return (date, formatterOptions) => {\n        // Allow tests to set a different locale and have that cause the formatter to be recreated\n        if (\n            !formatter ||\n            formatter.resolvedOptions().locale !== formatterOptions.locale ||\n            formatter.resolvedOptions().calendar !== formatterOptions.calendar\n        ) {\n            formatter = new Intl.DateTimeFormat(formatterOptions.locale, {\n                ...options,\n                calendar: options.calendar ?? formatterOptions.calendar,\n            });\n        }\n\n        const parts = formatter.formatToParts(date);\n        const part = parts.find((p) => p.type === type);\n        /* v8 ignore next - fallback value '' only for safety */\n        return part?.value ?? (fallback ? fallback(date, formatterOptions) : '');\n    };\n}\n\nfunction startOfWeek(date: Date, options: FormatterOptions) {\n    const locale = getLocale(options);\n    const weekInfo = getWeekInfo(locale);\n    const firstDay = weekInfo.firstDay === 7 ? 0 : weekInfo.firstDay;\n    const day = date.getDay();\n    const diff = (day < firstDay ? 7 : 0) + day - firstDay;\n    date.setDate(date.getDate() - diff);\n    date.setHours(0, 0, 0, 0);\n    return date;\n}\n\nfunction getWeekYear(date: Date, options: FormatterOptions) {\n    const locale = getLocale(options);\n    const minimalDays = getWeekInfo(locale).minimalDays;\n\n    const year = date.getFullYear();\n\n    const thisYear = startOfWeek(new Date(year, 0, minimalDays), options);\n    const nextYear = startOfWeek(new Date(year + 1, 0, minimalDays), options);\n\n    if (date.getTime() >= nextYear.getTime()) {\n        return year + 1;\n    } else if (date.getTime() >= thisYear.getTime()) {\n        return year;\n    } else {\n        return year - 1;\n    }\n}\n\nfunction getWeek(date: Date, options: FormatterOptions) {\n    const locale = getLocale(options);\n    const weekMs = 7 * 24 * 3600 * 1000;\n\n    const temp = startOfWeek(new Date(date), options);\n\n    const thisYear = new Date(getWeekYear(date, options), 0, getWeekInfo(locale).minimalDays);\n    startOfWeek(thisYear, options);\n\n    const diff = temp.getTime() - thisYear.getTime();\n    return Math.round(diff / weekMs) + 1;\n}\n\nfunction getWeekOfMonth(date: Date, options: FormatterOptions) {\n    const current = new Date(date);\n    current.setHours(0, 0, 0, 0);\n\n    const monthWeekStart = startOfWeek(new Date(date.getFullYear(), date.getMonth(), 1), options);\n\n    const weekMs = 7 * 24 * 3600 * 1000;\n    return Math.floor((date.getTime() - monthWeekStart.getTime()) / weekMs) + 1;\n}\n\nfunction getDayOfYear(date: Date) {\n    let days = 0;\n    for (let i = 0; i <= date.getMonth() - 1; i++) {\n        const temp = new Date(date.getFullYear(), i + 1, 0, 0, 0, 0);\n        days += temp.getDate();\n    }\n    return days + date.getDate();\n}\n\nfunction pad(length: number, val: string | number) {\n    return String(val).padStart(length, '0');\n}\n"
  },
  {
    "path": "lib/declarations/intl.d.ts",
    "content": "// TODO: Delete this file once Typescript has added these.\ndeclare namespace Intl {\n    // https://github.com/tc39/ecma402/pull/351\n    interface DateTimeFormatPartTypesRegistry {\n        yearName?: string;\n        relatedYear?: string;\n    }\n\n    /**\n     * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/getWeekInfo\n     */\n    interface WeekInfo {\n        firstDay: number;\n        weekend: readonly number[];\n        minimalDays: number;\n    }\n\n    interface Locale {\n        readonly weekInfo: WeekInfo;\n        getWeekInfo?: () => WeekInfo;\n    }\n}\n"
  },
  {
    "path": "lib/defaults.ts",
    "content": "// This file is meant to be a shared place for default configs.\n// It's read by the flow controllers, the executable, etc.\n// Refer to tests for the meaning of the different possible values.\n\nimport { SuccessCondition } from './completion-listener.js';\n\nexport const defaultInputTarget = 0;\n\n/**\n * Whether process.stdin should be forwarded to child processes.\n */\nexport const handleInput = false;\n\n/**\n * How many processes to run at once.\n */\nexport const maxProcesses = 0;\n\n/**\n * Indices and names of commands whose output are not to be logged.\n */\nexport const hide = '';\n\n/**\n * The character to split <names> on.\n */\nexport const nameSeparator = ',';\n\n/**\n * Which prefix style to use when logging processes output.\n */\nexport const prefix = '';\n\n/**\n * Default prefix color.\n * @see https://www.npmjs.com/package/chalk\n */\nexport const prefixColors = 'reset';\n\n/**\n * How many bytes we'll show on the command prefix.\n */\nexport const prefixLength = 10;\n\nexport const raw = false;\n\n/**\n * Number of attempts of restarting a process, if it exits with non-0 code.\n */\nexport const restartTries = 0;\n\n/**\n * How many milliseconds concurrently should wait before restarting a process.\n */\nexport const restartDelay = 0;\n\n/**\n * Condition of success for concurrently itself.\n */\nexport const success = 'all' as SuccessCondition;\n\n/**\n * Date format used when logging date/time.\n * @see https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table\n */\nexport const timestampFormat = 'yyyy-MM-dd HH:mm:ss.SSS';\n\n/**\n * Current working dir passed as option to spawn command.\n * Defaults to process.cwd()\n */\nexport const cwd: string | undefined = undefined;\n\n/**\n * Whether to show timing information for processes in console output.\n */\nexport const timings = false;\n\n/**\n * Passthrough additional arguments to commands (accessible via placeholders) instead of treating them as commands.\n */\nexport const passthroughArguments = false;\n\n/**\n * Signal to send to other processes if one exits or dies.\n *\n * Defaults to OS specific signal. (SIGTERM on Linux/MacOS)\n */\nexport const killSignal: string | undefined = undefined;\n"
  },
  {
    "path": "lib/flow-control/flow-controller.d.ts",
    "content": "import { Command } from '../command.js';\n\n/**\n * Interface for a class that controls and/or watches the behavior of commands.\n *\n * This may include logging their output, creating interactions between them, or changing when they\n * actually finish.\n */\nexport interface FlowController {\n    handle: (commands: Command[]) => { commands: Command[]; onFinish?: () => void | Promise<void> };\n}\n"
  },
  {
    "path": "lib/flow-control/input-handler.spec.ts",
    "content": "import { Buffer } from 'node:buffer';\nimport { PassThrough } from 'node:stream';\n\nimport { beforeEach, expect, it } from 'vitest';\n\nimport { createMockInstance } from '../__fixtures__/create-mock-instance.js';\nimport { FakeCommand } from '../__fixtures__/fake-command.js';\nimport { Command } from '../command.js';\nimport { Logger } from '../logger.js';\nimport { InputHandler } from './input-handler.js';\n\nlet commands: Command[];\nlet controller: InputHandler;\nlet inputStream: PassThrough;\nlet logger: Logger;\n\nbeforeEach(() => {\n    commands = [new FakeCommand('foo', 'echo foo', 0), new FakeCommand('bar', 'echo bar', 1)];\n    inputStream = new PassThrough();\n    logger = createMockInstance(Logger);\n    controller = new InputHandler({\n        defaultInputTarget: 0,\n        inputStream,\n        logger,\n    });\n});\n\nit('returns same commands', () => {\n    expect(controller.handle(commands)).toMatchObject({ commands });\n\n    controller = new InputHandler({ logger, inputStream });\n    expect(controller.handle(commands)).toMatchObject({ commands });\n});\n\nit('does nothing if called without input stream', () => {\n    new InputHandler({\n        defaultInputTarget: 0,\n        inputStream: undefined,\n        logger,\n    }).handle(commands);\n    inputStream.write('something');\n\n    expect(commands[0].stdin?.write).not.toHaveBeenCalled();\n});\n\nit('forwards input stream to default target ID', () => {\n    controller.handle(commands);\n\n    inputStream.write('something');\n\n    expect(commands[0].stdin?.write).toHaveBeenCalledExactlyOnceWith('something');\n    expect(commands[1].stdin?.write).not.toHaveBeenCalled();\n});\n\nit('forwards input stream to target index specified in input', () => {\n    controller.handle(commands);\n\n    inputStream.write('1:something');\n    inputStream.write('1:multi\\nline\\n');\n\n    expect(commands[0].stdin?.write).not.toHaveBeenCalled();\n    expect(commands[1].stdin?.write).toHaveBeenCalledTimes(2);\n    expect(commands[1].stdin?.write).toHaveBeenCalledWith('something');\n    expect(commands[1].stdin?.write).toHaveBeenCalledWith('multi\\nline\\n');\n});\n\nit('forwards input stream to target index specified in input when input contains colon', () => {\n    controller.handle(commands);\n\n    inputStream.emit('data', Buffer.from('1:some:thing'));\n    inputStream.emit('data', Buffer.from('1: :something'));\n    inputStream.emit('data', Buffer.from('1::something'));\n\n    expect(commands[0].stdin?.write).not.toHaveBeenCalled();\n    expect(commands[1].stdin?.write).toHaveBeenCalledTimes(3);\n    expect(commands[1].stdin?.write).toHaveBeenCalledWith('some:thing');\n    expect(commands[1].stdin?.write).toHaveBeenCalledWith(' :something');\n    expect(commands[1].stdin?.write).toHaveBeenCalledWith(':something');\n});\n\nit('does not forward input stream when input contains colon in a different format', () => {\n    controller.handle(commands);\n\n    inputStream.emit('data', Buffer.from('Ruby0::Const::Syntax'));\n    inputStream.emit('data', Buffer.from('1:Ruby1::Const::Syntax'));\n    inputStream.emit('data', Buffer.from('ruby_symbol_arg :my_symbol'));\n    inputStream.emit('data', Buffer.from('ruby_symbol_arg(:my_symbol)'));\n    inputStream.emit('data', Buffer.from('{ruby_key: :my_val}'));\n    inputStream.emit('data', Buffer.from('{:ruby_key=>:my_val}'));\n    inputStream.emit('data', Buffer.from('js_obj = {key: \"my_val\"}'));\n\n    expect(commands[1].stdin?.write).toHaveBeenCalledExactlyOnceWith('Ruby1::Const::Syntax');\n    expect(commands[0].stdin?.write).toHaveBeenCalledTimes(6);\n    expect(commands[0].stdin?.write).toHaveBeenCalledWith('Ruby0::Const::Syntax');\n    expect(commands[0].stdin?.write).toHaveBeenCalledWith('ruby_symbol_arg :my_symbol');\n    expect(commands[0].stdin?.write).toHaveBeenCalledWith('ruby_symbol_arg(:my_symbol)');\n    expect(commands[0].stdin?.write).toHaveBeenCalledWith('{ruby_key: :my_val}');\n    expect(commands[0].stdin?.write).toHaveBeenCalledWith('{:ruby_key=>:my_val}');\n    expect(commands[0].stdin?.write).toHaveBeenCalledWith('js_obj = {key: \"my_val\"}');\n});\n\nit('forwards input stream to target name specified in input', () => {\n    controller.handle(commands);\n\n    inputStream.write('bar:something');\n\n    expect(commands[0].stdin?.write).not.toHaveBeenCalled();\n    expect(commands[1].stdin?.write).toHaveBeenCalledExactlyOnceWith('something');\n});\n\nit('logs error if command has no stdin open', () => {\n    commands[0].stdin = undefined;\n    controller.handle(commands);\n\n    inputStream.write('something');\n\n    expect(commands[1].stdin?.write).not.toHaveBeenCalled();\n    expect(logger.logGlobalEvent).toHaveBeenCalledWith(\n        'Unable to find command \"0\", or it has no stdin open\\n',\n    );\n});\n\nit('fallback to default input stream if command is not found', () => {\n    controller.handle(commands);\n\n    inputStream.write('foobar:something');\n\n    expect(commands[0].stdin?.write).toHaveBeenCalledExactlyOnceWith('foobar:something');\n    expect(commands[1].stdin?.write).not.toHaveBeenCalled();\n    expect(logger.logGlobalEvent).not.toHaveBeenCalled();\n});\n\nit('pauses input stream when finished', () => {\n    expect(inputStream.readableFlowing).toBeNull();\n\n    const { onFinish } = controller.handle(commands);\n    expect(inputStream.readableFlowing).toBe(true);\n\n    expect(onFinish).toBeDefined();\n    onFinish?.();\n    expect(inputStream.readableFlowing).toBe(false);\n});\n\nit('does not pause input stream when pauseInputStreamOnFinish is set to false', () => {\n    controller = new InputHandler({ logger, inputStream, pauseInputStreamOnFinish: false });\n\n    expect(inputStream.readableFlowing).toBeNull();\n\n    const { onFinish } = controller.handle(commands);\n    expect(inputStream.readableFlowing).toBe(true);\n\n    expect(onFinish).toBeDefined();\n    onFinish?.();\n    expect(inputStream.readableFlowing).toBe(true);\n});\n"
  },
  {
    "path": "lib/flow-control/input-handler.ts",
    "content": "import { Readable } from 'node:stream';\n\nimport Rx from 'rxjs';\nimport { map } from 'rxjs/operators';\n\nimport { Command, CommandIdentifier } from '../command.js';\nimport * as defaults from '../defaults.js';\nimport { Logger } from '../logger.js';\nimport { FlowController } from './flow-controller.js';\n\n/**\n * Sends input from concurrently through to commands.\n *\n * Input can start with a command identifier, in which case it will be sent to that specific command.\n * For instance, `0:bla` will send `bla` to command at index `0`, and `server:stop` will send `stop`\n * to command with name `server`.\n *\n * If the input doesn't start with a command identifier, it is then always sent to the default target.\n */\nexport class InputHandler implements FlowController {\n    private readonly logger: Logger;\n    private readonly defaultInputTarget: CommandIdentifier;\n    private readonly inputStream?: Readable;\n    private readonly pauseInputStreamOnFinish: boolean;\n\n    constructor({\n        defaultInputTarget,\n        inputStream,\n        pauseInputStreamOnFinish,\n        logger,\n    }: {\n        inputStream?: Readable;\n        logger: Logger;\n        defaultInputTarget?: CommandIdentifier;\n        pauseInputStreamOnFinish?: boolean;\n    }) {\n        this.logger = logger;\n        this.defaultInputTarget = defaultInputTarget || defaults.defaultInputTarget;\n        this.inputStream = inputStream;\n        this.pauseInputStreamOnFinish = pauseInputStreamOnFinish !== false;\n    }\n\n    handle(commands: Command[]): {\n        commands: Command[];\n        onFinish?: () => void | undefined;\n    } {\n        const { inputStream } = this;\n        if (!inputStream) {\n            return { commands };\n        }\n\n        const commandsMap = new Map<string, Command>();\n        for (const command of commands) {\n            commandsMap.set(command.index.toString(), command);\n            commandsMap.set(command.name, command);\n        }\n\n        Rx.fromEvent(inputStream, 'data')\n            .pipe(map((data) => String(data)))\n            .subscribe((data) => {\n                const dataParts = data.split(/:(.+)/s);\n                let target = dataParts[0];\n                let command = commandsMap.get(target);\n                let input: string;\n\n                if (dataParts.length > 1 && command) {\n                    input = dataParts[1];\n                } else {\n                    // If `target` does not match a registered command,\n                    // fallback to `defaultInputTarget` and forward the whole input data\n                    target = this.defaultInputTarget.toString();\n                    command = commandsMap.get(target);\n                    input = data;\n                }\n\n                if (command?.stdin) {\n                    command.stdin.write(input);\n                } else {\n                    this.logger.logGlobalEvent(\n                        `Unable to find command \"${target}\", or it has no stdin open\\n`,\n                    );\n                }\n            });\n\n        return {\n            commands,\n            onFinish: () => {\n                if (this.pauseInputStreamOnFinish) {\n                    // https://github.com/kimmobrunfeldt/concurrently/issues/252\n                    inputStream.pause();\n                }\n            },\n        };\n    }\n}\n"
  },
  {
    "path": "lib/flow-control/kill-on-signal.spec.ts",
    "content": "import { EventEmitter } from 'node:events';\n\nimport { beforeEach, describe, expect, it, vi } from 'vitest';\n\nimport { createFakeCloseEvent, FakeCommand } from '../__fixtures__/fake-command.js';\nimport { Command } from '../command.js';\nimport { KillOnSignal } from './kill-on-signal.js';\n\nlet commands: Command[];\nlet controller: KillOnSignal;\nlet process: EventEmitter;\nlet abortController: AbortController;\nbeforeEach(() => {\n    process = new EventEmitter();\n    commands = [new FakeCommand(), new FakeCommand()];\n    abortController = new AbortController();\n    controller = new KillOnSignal({ process, abortController });\n});\n\nit('returns commands that keep non-close streams from original commands', () => {\n    const { commands: newCommands } = controller.handle(commands);\n    newCommands.forEach((newCommand, i) => {\n        expect(newCommand.close).not.toBe(commands[i].close);\n        expect(newCommand.error).toBe(commands[i].error);\n        expect(newCommand.stdout).toBe(commands[i].stdout);\n        expect(newCommand.stderr).toBe(commands[i].stderr);\n    });\n});\n\nit('returns commands that map SIGINT to exit code 0', () => {\n    const { commands: newCommands } = controller.handle(commands);\n    expect(newCommands).not.toBe(commands);\n    expect(newCommands).toHaveLength(commands.length);\n\n    const callback = vi.fn();\n    newCommands[0].close.subscribe(callback);\n    process.emit('SIGINT', 'SIGINT');\n\n    // A fake command's .kill() call won't trigger a close event automatically...\n    commands[0].close.next(createFakeCloseEvent({ exitCode: 1 }));\n\n    expect(callback).not.toHaveBeenCalledWith(expect.objectContaining({ exitCode: 'SIGINT' }));\n    expect(callback).toHaveBeenCalledWith(expect.objectContaining({ exitCode: 0 }));\n});\n\nit('returns commands that keep non-SIGINT exit codes', () => {\n    const { commands: newCommands } = controller.handle(commands);\n    expect(newCommands).not.toBe(commands);\n    expect(newCommands).toHaveLength(commands.length);\n\n    const callback = vi.fn();\n    newCommands[0].close.subscribe(callback);\n    commands[0].close.next(createFakeCloseEvent({ exitCode: 1 }));\n\n    expect(callback).toHaveBeenCalledWith(expect.objectContaining({ exitCode: 1 }));\n});\n\ndescribe.each(['SIGINT', 'SIGTERM', 'SIGHUP'])('on %s', (signal) => {\n    it('kills all commands', () => {\n        controller.handle(commands);\n        process.emit(signal, signal);\n\n        expect(process.listenerCount(signal)).toBe(1);\n        expect(commands[0].kill).toHaveBeenCalledWith(signal);\n        expect(commands[1].kill).toHaveBeenCalledWith(signal);\n    });\n\n    it('sends abort signal', () => {\n        controller.handle(commands);\n        process.emit(signal, signal);\n\n        expect(abortController.signal.aborted).toBe(true);\n    });\n\n    it('removes event listener on finish', () => {\n        const { onFinish } = controller.handle(commands);\n        onFinish();\n        expect(process.listenerCount(signal)).toBe(0);\n    });\n});\n"
  },
  {
    "path": "lib/flow-control/kill-on-signal.ts",
    "content": "import EventEmitter from 'node:events';\n\nimport { map } from 'rxjs/operators';\n\nimport { Command } from '../command.js';\nimport { FlowController } from './flow-controller.js';\n\nconst SIGNALS = ['SIGINT', 'SIGTERM', 'SIGHUP'] as const;\n\n/**\n * Watches the main concurrently process for signals and sends the same signal down to each spawned\n * command.\n */\nexport class KillOnSignal implements FlowController {\n    private readonly process: EventEmitter;\n    private readonly abortController?: AbortController;\n\n    constructor({\n        process,\n        abortController,\n    }: {\n        process: EventEmitter;\n        abortController?: AbortController;\n    }) {\n        this.process = process;\n        this.abortController = abortController;\n    }\n\n    handle(commands: Command[]) {\n        let caughtSignal: NodeJS.Signals;\n        const signalListener = (signal: NodeJS.Signals) => {\n            caughtSignal = signal;\n            this.abortController?.abort();\n            commands.forEach((command) => command.kill(signal));\n        };\n        SIGNALS.forEach((signal) => this.process.on(signal, signalListener));\n\n        return {\n            commands: commands.map((command) => {\n                const closeStream = command.close.pipe(\n                    map((exitInfo) => {\n                        const exitCode = caughtSignal === 'SIGINT' ? 0 : exitInfo.exitCode;\n                        return { ...exitInfo, exitCode };\n                    }),\n                );\n                // Return a proxy so that mutations happen on the original Command object.\n                // If either `Object.assign()` or `Object.create()` were used, it'd be hard to\n                // reflect the mutations on Command objects referenced by previous flow controllers.\n                return new Proxy(command, {\n                    get(target, prop: keyof Command) {\n                        return prop === 'close' ? closeStream : target[prop];\n                    },\n                });\n            }),\n            onFinish: () => {\n                // Avoids MaxListenersExceededWarning when running programmatically\n                SIGNALS.forEach((signal) => this.process.off(signal, signalListener));\n            },\n        };\n    }\n}\n"
  },
  {
    "path": "lib/flow-control/kill-others.spec.ts",
    "content": "import { beforeEach, describe, expect, it, vi } from 'vitest';\n\nimport { createMockInstance } from '../__fixtures__/create-mock-instance.js';\nimport {\n    createFakeCloseEvent,\n    createFakeProcess,\n    FakeCommand,\n} from '../__fixtures__/fake-command.js';\nimport { Logger } from '../logger.js';\nimport { KillOthers, ProcessCloseCondition } from './kill-others.js';\n\nlet commands: FakeCommand[];\nlet logger: Logger;\nlet abortController: AbortController;\nbeforeEach(() => {\n    commands = [new FakeCommand(), new FakeCommand()];\n    logger = createMockInstance(Logger);\n    abortController = new AbortController();\n});\n\nconst createWithConditions = (\n    conditions: ProcessCloseCondition[],\n    opts?: { timeoutMs?: number; killSignal?: string },\n) =>\n    new KillOthers({\n        logger,\n        abortController,\n        conditions,\n        killSignal: undefined,\n        ...opts,\n    });\n\nconst assignProcess = (command: FakeCommand) => {\n    const process = createFakeProcess(1);\n    command.pid = process.pid;\n    command.process = process;\n};\n\nconst unassignProcess = (command: FakeCommand) => {\n    command.pid = undefined;\n    command.process = undefined;\n};\n\nit('returns same commands', () => {\n    expect(createWithConditions(['success']).handle(commands)).toMatchObject({ commands });\n    expect(createWithConditions(['failure']).handle(commands)).toMatchObject({ commands });\n});\n\nit('does not kill others if condition does not match', () => {\n    createWithConditions(['failure']).handle(commands);\n    assignProcess(commands[1]);\n    commands[0].close.next(createFakeCloseEvent({ exitCode: 0 }));\n\n    expect(logger.logGlobalEvent).not.toHaveBeenCalled();\n    expect(commands[0].kill).not.toHaveBeenCalled();\n    expect(commands[1].kill).not.toHaveBeenCalled();\n});\n\ndescribe.each(['success', 'failure'] as const)('on %s', (condition) => {\n    const exitCode = condition === 'success' ? 0 : 1;\n    const inversedCode = exitCode === 1 ? 0 : 1;\n\n    it('kills other processes', () => {\n        createWithConditions([condition]).handle(commands);\n        assignProcess(commands[1]);\n        commands[0].close.next(createFakeCloseEvent({ exitCode }));\n\n        expect(logger.logGlobalEvent).toHaveBeenCalledExactlyOnceWith(\n            'Sending SIGTERM to other processes..',\n        );\n        expect(commands[0].kill).not.toHaveBeenCalled();\n        expect(commands[1].kill).toHaveBeenCalledWith(undefined);\n    });\n\n    it('kills other processes, with specified signal', () => {\n        createWithConditions([condition], { killSignal: 'SIGKILL' }).handle(commands);\n        assignProcess(commands[1]);\n        commands[0].close.next(createFakeCloseEvent({ exitCode }));\n\n        expect(logger.logGlobalEvent).toHaveBeenCalledExactlyOnceWith(\n            'Sending SIGKILL to other processes..',\n        );\n        expect(commands[0].kill).not.toHaveBeenCalled();\n        expect(commands[1].kill).toHaveBeenCalledWith('SIGKILL');\n    });\n\n    it('sends abort signal on condition match', () => {\n        createWithConditions([condition]).handle(commands);\n        commands[0].close.next(createFakeCloseEvent({ exitCode }));\n\n        expect(abortController.signal.aborted).toBe(true);\n    });\n\n    it('does not send abort signal on condition mismatch', () => {\n        createWithConditions([condition]).handle(commands);\n        commands[0].close.next(createFakeCloseEvent({ exitCode: inversedCode }));\n\n        expect(abortController.signal.aborted).toBe(false);\n    });\n});\n\nit('does nothing if called without conditions', () => {\n    createWithConditions([]).handle(commands);\n    commands[0].close.next(createFakeCloseEvent({ exitCode: 0 }));\n\n    expect(logger.logGlobalEvent).not.toHaveBeenCalled();\n    expect(commands[0].kill).not.toHaveBeenCalled();\n    expect(commands[1].kill).not.toHaveBeenCalled();\n});\n\nit('does not try to kill processes already dead', () => {\n    createWithConditions(['failure']).handle(commands);\n    commands[0].close.next(createFakeCloseEvent({ exitCode: 1 }));\n\n    expect(logger.logGlobalEvent).not.toHaveBeenCalled();\n    expect(commands[0].kill).not.toHaveBeenCalled();\n    expect(commands[1].kill).not.toHaveBeenCalled();\n});\n\nit('force kills misbehaving processes after a timeout', () => {\n    vi.useFakeTimers();\n    commands.push(new FakeCommand());\n\n    createWithConditions(['failure'], { timeoutMs: 500 }).handle(commands);\n    assignProcess(commands[1]);\n    assignProcess(commands[2]);\n    commands[2].kill = vi.fn(() => unassignProcess(commands[2]));\n    commands[0].close.next(createFakeCloseEvent({ exitCode: 1 }));\n\n    vi.advanceTimersByTime(500);\n\n    expect(commands[1].kill).toHaveBeenCalledTimes(2);\n    expect(commands[1].kill).toHaveBeenCalledWith('SIGKILL');\n    expect(commands[2].kill).toHaveBeenCalledTimes(1);\n});\n"
  },
  {
    "path": "lib/flow-control/kill-others.ts",
    "content": "import { filter, map } from 'rxjs/operators';\n\nimport { Command } from '../command.js';\nimport { Logger } from '../logger.js';\nimport { castArray } from '../utils.js';\nimport { FlowController } from './flow-controller.js';\n\nexport type ProcessCloseCondition = 'failure' | 'success';\n\n/**\n * Sends a SIGTERM signal to all commands when one of the commands exits with a matching condition.\n */\nexport class KillOthers implements FlowController {\n    private readonly logger: Logger;\n    private readonly abortController?: AbortController;\n    private readonly conditions: ProcessCloseCondition[];\n    private readonly killSignal: string | undefined;\n    private readonly timeoutMs?: number;\n\n    constructor({\n        logger,\n        abortController,\n        conditions,\n        killSignal,\n        timeoutMs,\n    }: {\n        logger: Logger;\n        abortController?: AbortController;\n        conditions: ProcessCloseCondition | ProcessCloseCondition[];\n        killSignal: string | undefined;\n        timeoutMs?: number;\n    }) {\n        this.logger = logger;\n        this.abortController = abortController;\n        this.conditions = castArray(conditions);\n        this.killSignal = killSignal;\n        this.timeoutMs = timeoutMs;\n    }\n\n    handle(commands: Command[]) {\n        const conditions = this.conditions.filter(\n            (condition) => condition === 'failure' || condition === 'success',\n        );\n\n        if (!conditions.length) {\n            return { commands };\n        }\n\n        const closeStates = commands.map((command) =>\n            command.close.pipe(\n                map(({ exitCode }) =>\n                    exitCode === 0 ? ('success' as const) : ('failure' as const),\n                ),\n                filter((state) => conditions.includes(state)),\n            ),\n        );\n\n        closeStates.forEach((closeState) =>\n            closeState.subscribe(() => {\n                this.abortController?.abort();\n\n                const killableCommands = commands.filter((command) => Command.canKill(command));\n                if (killableCommands.length) {\n                    this.logger.logGlobalEvent(\n                        `Sending ${this.killSignal || 'SIGTERM'} to other processes..`,\n                    );\n                    killableCommands.forEach((command) => command.kill(this.killSignal));\n                    this.maybeForceKill(killableCommands);\n                }\n            }),\n        );\n\n        return { commands };\n    }\n\n    private maybeForceKill(commands: Command[]) {\n        // No need to force kill when the signal already is SIGKILL.\n        if (!this.timeoutMs || this.killSignal === 'SIGKILL') {\n            return;\n        }\n\n        setTimeout(() => {\n            const killableCommands = commands.filter((command) => Command.canKill(command));\n            if (killableCommands) {\n                this.logger.logGlobalEvent(\n                    `Sending SIGKILL to ${killableCommands.length} processes..`,\n                );\n                killableCommands.forEach((command) => command.kill('SIGKILL'));\n            }\n        }, this.timeoutMs);\n    }\n}\n"
  },
  {
    "path": "lib/flow-control/log-error.spec.ts",
    "content": "import { beforeEach, expect, it } from 'vitest';\n\nimport { createMockInstance } from '../__fixtures__/create-mock-instance.js';\nimport { FakeCommand } from '../__fixtures__/fake-command.js';\nimport { Logger } from '../logger.js';\nimport { LogError } from './log-error.js';\n\nlet controller: LogError;\nlet logger: Logger;\nlet commands: FakeCommand[];\nbeforeEach(() => {\n    commands = [new FakeCommand(), new FakeCommand(), new FakeCommand()];\n\n    logger = createMockInstance(Logger);\n    controller = new LogError({ logger });\n});\n\nit('returns same commands', () => {\n    expect(controller.handle(commands)).toMatchObject({ commands });\n});\n\nit('logs the error event of each command', () => {\n    controller.handle(commands);\n    commands[0].error.next('error from command 0');\n\n    const error1 = new Error('test');\n    commands[1].error.next(error1);\n\n    // Testing Error without stack\n    const error2 = new Error('test');\n    error2.stack = '';\n    commands[2].error.next(error2);\n\n    expect(logger.logCommandEvent).toHaveBeenCalledTimes(6);\n    expect(logger.logCommandEvent).toHaveBeenCalledWith(\n        `Error occurred when executing command: ${commands[0].command}`,\n        commands[0],\n    );\n    expect(logger.logCommandEvent).toHaveBeenCalledWith('error from command 0', commands[0]);\n\n    expect(logger.logCommandEvent).toHaveBeenCalledWith(\n        `Error occurred when executing command: ${commands[1].command}`,\n        commands[1],\n    );\n    expect(logger.logCommandEvent).toHaveBeenCalledWith(error1.stack, commands[1]);\n\n    expect(logger.logCommandEvent).toHaveBeenCalledWith(\n        `Error occurred when executing command: ${commands[2].command}`,\n        commands[2],\n    );\n    expect(logger.logCommandEvent).toHaveBeenCalledWith(String(error2), commands[2]);\n});\n"
  },
  {
    "path": "lib/flow-control/log-error.ts",
    "content": "import { Command } from '../command.js';\nimport { Logger } from '../logger.js';\nimport { FlowController } from './flow-controller.js';\n\n/**\n * Logs when commands failed executing, e.g. due to the executable not existing in the system.\n */\nexport class LogError implements FlowController {\n    private readonly logger: Logger;\n\n    constructor({ logger }: { logger: Logger }) {\n        this.logger = logger;\n    }\n\n    handle(commands: Command[]) {\n        commands.forEach((command) =>\n            command.error.subscribe((event) => {\n                this.logger.logCommandEvent(\n                    `Error occurred when executing command: ${command.command}`,\n                    command,\n                );\n\n                const errorText = String(event instanceof Error ? event.stack || event : event);\n                this.logger.logCommandEvent(errorText, command);\n            }),\n        );\n\n        return { commands };\n    }\n}\n"
  },
  {
    "path": "lib/flow-control/log-exit.spec.ts",
    "content": "import { beforeEach, expect, it } from 'vitest';\n\nimport { createMockInstance } from '../__fixtures__/create-mock-instance.js';\nimport { createFakeCloseEvent, FakeCommand } from '../__fixtures__/fake-command.js';\nimport { Logger } from '../logger.js';\nimport { LogExit } from './log-exit.js';\n\nlet controller: LogExit;\nlet logger: Logger;\nlet commands: FakeCommand[];\nbeforeEach(() => {\n    commands = [new FakeCommand(), new FakeCommand()];\n\n    logger = createMockInstance(Logger);\n    controller = new LogExit({ logger });\n});\n\nit('returns same commands', () => {\n    expect(controller.handle(commands)).toMatchObject({ commands });\n});\n\nit('logs the close event of each command', () => {\n    controller.handle(commands);\n\n    commands[0].close.next(createFakeCloseEvent({ exitCode: 0 }));\n    commands[1].close.next(createFakeCloseEvent({ exitCode: 'SIGTERM' }));\n\n    expect(logger.logCommandEvent).toHaveBeenCalledTimes(2);\n    expect(logger.logCommandEvent).toHaveBeenCalledWith(\n        `${commands[0].command} exited with code 0`,\n        commands[0],\n    );\n    expect(logger.logCommandEvent).toHaveBeenCalledWith(\n        `${commands[1].command} exited with code SIGTERM`,\n        commands[1],\n    );\n});\n"
  },
  {
    "path": "lib/flow-control/log-exit.ts",
    "content": "import { Command } from '../command.js';\nimport { Logger } from '../logger.js';\nimport { FlowController } from './flow-controller.js';\n\n/**\n * Logs the exit code/signal of commands.\n */\nexport class LogExit implements FlowController {\n    private readonly logger: Logger;\n\n    constructor({ logger }: { logger: Logger }) {\n        this.logger = logger;\n    }\n\n    handle(commands: Command[]) {\n        commands.forEach((command) =>\n            command.close.subscribe(({ exitCode }) => {\n                this.logger.logCommandEvent(\n                    `${command.command} exited with code ${exitCode}`,\n                    command,\n                );\n            }),\n        );\n\n        return { commands };\n    }\n}\n"
  },
  {
    "path": "lib/flow-control/log-output.spec.ts",
    "content": "import { Buffer } from 'node:buffer';\n\nimport { beforeEach, expect, it } from 'vitest';\n\nimport { createMockInstance } from '../__fixtures__/create-mock-instance.js';\nimport { FakeCommand } from '../__fixtures__/fake-command.js';\nimport { Logger } from '../logger.js';\nimport { LogOutput } from './log-output.js';\n\nlet controller: LogOutput;\nlet logger: Logger;\nlet commands: FakeCommand[];\nbeforeEach(() => {\n    commands = [new FakeCommand(), new FakeCommand()];\n\n    logger = createMockInstance(Logger);\n    controller = new LogOutput({ logger });\n});\n\nit('returns same commands', () => {\n    expect(controller.handle(commands)).toMatchObject({ commands });\n});\n\nit('logs the stdout of each command', () => {\n    controller.handle(commands);\n\n    commands[0].stdout.next(Buffer.from('foo'));\n    commands[1].stdout.next(Buffer.from('bar'));\n\n    expect(logger.logCommandText).toHaveBeenCalledTimes(2);\n    expect(logger.logCommandText).toHaveBeenCalledWith('foo', commands[0]);\n    expect(logger.logCommandText).toHaveBeenCalledWith('bar', commands[1]);\n});\n\nit('logs the stderr of each command', () => {\n    controller.handle(commands);\n\n    commands[0].stderr.next(Buffer.from('foo'));\n    commands[1].stderr.next(Buffer.from('bar'));\n\n    expect(logger.logCommandText).toHaveBeenCalledTimes(2);\n    expect(logger.logCommandText).toHaveBeenCalledWith('foo', commands[0]);\n    expect(logger.logCommandText).toHaveBeenCalledWith('bar', commands[1]);\n});\n"
  },
  {
    "path": "lib/flow-control/log-output.ts",
    "content": "import { Command } from '../command.js';\nimport { Logger } from '../logger.js';\nimport { FlowController } from './flow-controller.js';\n\n/**\n * Logs the stdout and stderr output of commands.\n */\nexport class LogOutput implements FlowController {\n    private readonly logger: Logger;\n    constructor({ logger }: { logger: Logger }) {\n        this.logger = logger;\n    }\n\n    handle(commands: Command[]) {\n        commands.forEach((command) => {\n            command.stdout.subscribe((text) =>\n                this.logger.logCommandText(text.toString(), command),\n            );\n            command.stderr.subscribe((text) =>\n                this.logger.logCommandText(text.toString(), command),\n            );\n        });\n\n        return { commands };\n    }\n}\n"
  },
  {
    "path": "lib/flow-control/log-timings.spec.ts",
    "content": "import { beforeEach, expect, it } from 'vitest';\n\nimport { createMockInstance } from '../__fixtures__/create-mock-instance.js';\nimport { createFakeCloseEvent, FakeCommand } from '../__fixtures__/fake-command.js';\nimport { CloseEvent } from '../command.js';\nimport { DateFormatter } from '../date-format.js';\nimport { Logger } from '../logger.js';\nimport { LogTimings } from './log-timings.js';\n\n// shown in timing order\nconst startDate0 = new Date();\nconst startDate1 = new Date(startDate0.getTime() + 1000);\nconst endDate1 = new Date(startDate0.getTime() + 5000);\nconst endDate0 = new Date(startDate0.getTime() + 3000);\n\nconst timestampFormat = 'yyyy-MM-dd HH:mm:ss.SSS';\nconst getDurationText = (startDate: Date, endDate: Date) =>\n    `${(endDate.getTime() - startDate.getTime()).toLocaleString()}ms`;\nconst command0DurationTextMs = getDurationText(startDate0, endDate0);\nconst command1DurationTextMs = getDurationText(startDate1, endDate1);\n\nlet controller: LogTimings;\nlet logger: Logger;\nlet commands: FakeCommand[];\nlet command0ExitInfo: CloseEvent;\nlet command1ExitInfo: CloseEvent;\n\nbeforeEach(() => {\n    commands = [new FakeCommand('foo', 'command 1', 0), new FakeCommand('bar', 'command 2', 1)];\n\n    command0ExitInfo = createFakeCloseEvent({\n        command: commands[0],\n        timings: {\n            startDate: startDate0,\n            endDate: endDate0,\n            durationSeconds: endDate0.getTime() - startDate0.getTime(),\n        },\n        index: commands[0].index,\n    });\n\n    command1ExitInfo = createFakeCloseEvent({\n        command: commands[1],\n        timings: {\n            startDate: startDate1,\n            endDate: endDate1,\n            durationSeconds: endDate1.getTime() - startDate1.getTime(),\n        },\n        index: commands[1].index,\n    });\n\n    logger = createMockInstance(Logger);\n    controller = new LogTimings({ logger, timestampFormat });\n});\n\nit('returns same commands', () => {\n    expect(controller.handle(commands)).toMatchObject({ commands });\n});\n\nit(\"does not log timings and doesn't throw if no logger is provided\", () => {\n    controller = new LogTimings({});\n    const { onFinish } = controller.handle(commands);\n\n    commands[0].timer.next({ startDate: startDate0 });\n    commands[1].timer.next({ startDate: startDate1 });\n    commands[1].timer.next({ startDate: startDate1, endDate: endDate1 });\n    commands[0].timer.next({ startDate: startDate0, endDate: endDate0 });\n\n    onFinish?.();\n\n    expect(logger.logCommandEvent).toHaveBeenCalledTimes(0);\n});\n\nit('logs the timings at the start and end (ie complete or error) event of each command', () => {\n    const formatter = new DateFormatter(timestampFormat);\n    controller.handle(commands);\n\n    commands[0].timer.next({ startDate: startDate0 });\n    commands[1].timer.next({ startDate: startDate1 });\n    commands[1].timer.next({ startDate: startDate1, endDate: endDate1 });\n    commands[0].timer.next({ startDate: startDate0, endDate: endDate0 });\n\n    expect(logger.logCommandEvent).toHaveBeenCalledTimes(4);\n    expect(logger.logCommandEvent).toHaveBeenCalledWith(\n        `${commands[0].command} started at ${formatter.format(startDate0)}`,\n        commands[0],\n    );\n    expect(logger.logCommandEvent).toHaveBeenCalledWith(\n        `${commands[1].command} started at ${formatter.format(startDate1)}`,\n        commands[1],\n    );\n    expect(logger.logCommandEvent).toHaveBeenCalledWith(\n        `${commands[1].command} stopped at ${formatter.format(\n            endDate1,\n        )} after ${command1DurationTextMs}`,\n        commands[1],\n    );\n    expect(logger.logCommandEvent).toHaveBeenCalledWith(\n        `${commands[0].command} stopped at ${formatter.format(\n            endDate0,\n        )} after ${command0DurationTextMs}`,\n        commands[0],\n    );\n});\n\nit('does not log timings summary if there was an error', () => {\n    const { onFinish } = controller.handle(commands);\n\n    commands[0].close.next(command0ExitInfo);\n    commands[1].error.next(undefined);\n\n    onFinish?.();\n\n    expect(logger.logTable).toHaveBeenCalledTimes(0);\n});\n\nit('logs the sorted timings summary when all processes close successfully after onFinish is called', () => {\n    const { onFinish } = controller.handle(commands);\n\n    commands[0].close.next(command0ExitInfo);\n    commands[1].close.next(command1ExitInfo);\n\n    expect(logger.logGlobalEvent).toHaveBeenCalledTimes(0);\n\n    onFinish?.();\n\n    expect(logger.logGlobalEvent).toHaveBeenCalledExactlyOnceWith('Timings:');\n    // sorted by duration\n    expect(logger.logTable).toHaveBeenCalledExactlyOnceWith([\n        LogTimings.mapCloseEventToTimingInfo(command1ExitInfo),\n        LogTimings.mapCloseEventToTimingInfo(command0ExitInfo),\n    ]);\n});\n"
  },
  {
    "path": "lib/flow-control/log-timings.ts",
    "content": "import assert from 'node:assert';\n\nimport Rx from 'rxjs';\nimport { bufferCount, combineLatestWith, take } from 'rxjs/operators';\n\nimport { CloseEvent, Command } from '../command.js';\nimport { DateFormatter } from '../date-format.js';\nimport * as defaults from '../defaults.js';\nimport { Logger } from '../logger.js';\nimport { FlowController } from './flow-controller.js';\n\ntype TimingInfo = {\n    name: string;\n    duration: string;\n    'exit code': string | number;\n    killed: boolean;\n    command: string;\n};\n\n/**\n * Logs timing information about commands as they start/stop and then a summary when all commands finish.\n */\nexport class LogTimings implements FlowController {\n    static mapCloseEventToTimingInfo({\n        command,\n        timings,\n        killed,\n        exitCode,\n    }: CloseEvent): TimingInfo {\n        const readableDurationMs = (\n            timings.endDate.getTime() - timings.startDate.getTime()\n        ).toLocaleString();\n        return {\n            name: command.name,\n            duration: readableDurationMs,\n            'exit code': exitCode,\n            killed,\n            command: command.command,\n        };\n    }\n\n    private readonly logger?: Logger;\n    private readonly dateFormatter: DateFormatter;\n\n    constructor({\n        logger,\n        timestampFormat = defaults.timestampFormat,\n    }: {\n        logger?: Logger;\n        timestampFormat?: string;\n    }) {\n        this.logger = logger;\n        this.dateFormatter = new DateFormatter(timestampFormat);\n    }\n\n    private printExitInfoTimingTable(exitInfos: CloseEvent[]) {\n        assert.ok(this.logger);\n\n        const exitInfoTable = exitInfos\n            .sort((a, b) => b.timings.durationSeconds - a.timings.durationSeconds)\n            .map(LogTimings.mapCloseEventToTimingInfo);\n\n        this.logger.logGlobalEvent('Timings:');\n        this.logger.logTable(exitInfoTable);\n        return exitInfos;\n    }\n\n    handle(commands: Command[]) {\n        const { logger } = this;\n        if (!logger) {\n            return { commands };\n        }\n\n        // individual process timings\n        commands.forEach((command) => {\n            command.timer.subscribe(({ startDate, endDate }) => {\n                if (!endDate) {\n                    const formattedStartDate = this.dateFormatter.format(startDate);\n                    logger.logCommandEvent(\n                        `${command.command} started at ${formattedStartDate}`,\n                        command,\n                    );\n                } else {\n                    const durationMs = endDate.getTime() - startDate.getTime();\n                    const formattedEndDate = this.dateFormatter.format(endDate);\n                    logger.logCommandEvent(\n                        `${\n                            command.command\n                        } stopped at ${formattedEndDate} after ${durationMs.toLocaleString()}ms`,\n                        command,\n                    );\n                }\n            });\n        });\n\n        // overall summary timings\n        const closeStreams = commands.map((command) => command.close);\n        const finished = new Rx.Subject<void>();\n        const allProcessesClosed = Rx.merge(...closeStreams).pipe(\n            bufferCount(closeStreams.length),\n            take(1),\n            combineLatestWith(finished),\n        );\n        allProcessesClosed.subscribe(([exitInfos]) => this.printExitInfoTimingTable(exitInfos));\n        return { commands, onFinish: () => finished.next() };\n    }\n}\n"
  },
  {
    "path": "lib/flow-control/logger-padding.spec.ts",
    "content": "import { beforeEach, expect, it, MockedObject } from 'vitest';\n\nimport { createMockInstance } from '../__fixtures__/create-mock-instance.js';\nimport { FakeCommand } from '../__fixtures__/fake-command.js';\nimport { Logger } from '../logger.js';\nimport { LoggerPadding } from './logger-padding.js';\n\nlet logger: MockedObject<Logger>;\nlet controller: LoggerPadding;\nlet commands: FakeCommand[];\n\nbeforeEach(() => {\n    commands = [new FakeCommand(), new FakeCommand()];\n    logger = createMockInstance(Logger);\n    controller = new LoggerPadding({ logger });\n});\n\nit('returns same commands', () => {\n    expect(controller.handle(commands)).toMatchObject({ commands });\n});\n\nit('sets the prefix length on handle', () => {\n    controller.handle(commands);\n    expect(logger.setPrefixLength).toHaveBeenCalledTimes(1);\n});\n\nit('updates the prefix length when commands emit a start timer', () => {\n    controller.handle(commands);\n    commands[0].timer.next({ startDate: new Date() });\n    expect(logger.setPrefixLength).toHaveBeenCalledTimes(2);\n\n    commands[1].timer.next({ startDate: new Date() });\n    expect(logger.setPrefixLength).toHaveBeenCalledTimes(3);\n});\n\nit('sets prefix length to the longest prefix of all commands', () => {\n    logger.getPrefixContent\n        .mockReturnValueOnce({ type: 'default', value: 'foobar' })\n        .mockReturnValueOnce({ type: 'default', value: 'baz' });\n\n    controller.handle(commands);\n    expect(logger.setPrefixLength).toHaveBeenCalledWith(6);\n});\n\nit('does not shorten the prefix length', () => {\n    logger.getPrefixContent\n        .mockReturnValueOnce({ type: 'default', value: '100' })\n        .mockReturnValueOnce({ type: 'default', value: '1' });\n\n    controller.handle(commands);\n    commands[0].timer.next({ startDate: new Date() });\n    expect(logger.setPrefixLength).toHaveBeenCalledWith(3);\n\n    commands[0].timer.next({ startDate: new Date() });\n    expect(logger.setPrefixLength).toHaveBeenCalledWith(3);\n});\n\nit('unsubscribes from start timers on finish', () => {\n    logger.getPrefixContent.mockReturnValue({ type: 'default', value: '1' });\n\n    const { onFinish } = controller.handle(commands);\n    commands[0].timer.next({ startDate: new Date() });\n    expect(logger.setPrefixLength).toHaveBeenCalledTimes(2);\n\n    onFinish();\n    commands[0].timer.next({ startDate: new Date() });\n    expect(logger.setPrefixLength).toHaveBeenCalledTimes(2);\n});\n"
  },
  {
    "path": "lib/flow-control/logger-padding.ts",
    "content": "import { Command } from '../command.js';\nimport { Logger } from '../logger.js';\nimport { FlowController } from './flow-controller.js';\n\nexport class LoggerPadding implements FlowController {\n    private readonly logger: Logger;\n\n    constructor({ logger }: { logger: Logger }) {\n        this.logger = logger;\n    }\n\n    handle(commands: Command[]): { commands: Command[]; onFinish: () => void } {\n        // Sometimes there's limited concurrency, so not all commands will spawn straight away.\n        // Compute the prefix length now, which works for all styles but those with a PID.\n        let length = commands.reduce((length, command) => {\n            const content = this.logger.getPrefixContent(command);\n            return Math.max(length, content?.value.length || 0);\n        }, 0);\n        this.logger.setPrefixLength(length);\n\n        // The length of prefixes is somewhat stable, except for PIDs, which might change when a\n        // process spawns (e.g. PIDs might look like 1, 10 or 100), therefore listen to command starts\n        // and update the prefix length when this happens.\n        const subs = commands.map((command) =>\n            command.timer.subscribe((event) => {\n                if (!event.endDate) {\n                    const content = this.logger.getPrefixContent(command);\n                    length = Math.max(length, content?.value.length || 0);\n                    this.logger.setPrefixLength(length);\n                }\n            }),\n        );\n\n        return {\n            commands,\n            onFinish() {\n                subs.forEach((sub) => sub.unsubscribe());\n            },\n        };\n    }\n}\n"
  },
  {
    "path": "lib/flow-control/output-error-handler.spec.ts",
    "content": "import { Writable } from 'node:stream';\n\nimport { beforeEach, describe, expect, it, vi } from 'vitest';\n\nimport { FakeCommand } from '../__fixtures__/fake-command.js';\nimport { OutputErrorHandler } from './output-error-handler.js';\n\nlet controller: OutputErrorHandler;\nlet outputStream: Writable;\nlet abortController: AbortController;\nlet commands: FakeCommand[];\nbeforeEach(() => {\n    commands = [new FakeCommand(), new FakeCommand()];\n\n    abortController = new AbortController();\n    outputStream = new Writable();\n    controller = new OutputErrorHandler({ abortController, outputStream });\n});\n\nit('returns same commands', () => {\n    expect(controller.handle(commands)).toMatchObject({ commands });\n});\n\ndescribe('on output stream error', () => {\n    beforeEach(() => {\n        controller.handle(commands);\n        outputStream.emit('error', new Error('test'));\n    });\n\n    it('kills every command', () => {\n        expect(commands[0].kill).toHaveBeenCalled();\n        expect(commands[1].kill).toHaveBeenCalled();\n    });\n\n    it('sends abort signal', () => {\n        expect(abortController.signal.aborted).toBe(true);\n    });\n});\n\ndescribe('on finish', () => {\n    it('unsubscribes from output stream error', () => {\n        const { onFinish } = controller.handle(commands);\n        onFinish();\n\n        outputStream.on('error', vi.fn());\n        outputStream.emit('error', new Error('test'));\n\n        expect(commands[0].kill).not.toHaveBeenCalled();\n        expect(commands[1].kill).not.toHaveBeenCalled();\n        expect(abortController.signal.aborted).toBe(false);\n    });\n});\n"
  },
  {
    "path": "lib/flow-control/output-error-handler.ts",
    "content": "import { Writable } from 'node:stream';\n\nimport { Command } from '../command.js';\nimport { fromSharedEvent } from '../observables.js';\nimport { FlowController } from './flow-controller.js';\n\n/**\n * Kills processes and aborts further command spawning on output stream error (namely, SIGPIPE).\n */\nexport class OutputErrorHandler implements FlowController {\n    private readonly outputStream: Writable;\n    private readonly abortController: AbortController;\n\n    constructor({\n        abortController,\n        outputStream,\n    }: {\n        abortController: AbortController;\n        outputStream: Writable;\n    }) {\n        this.abortController = abortController;\n        this.outputStream = outputStream;\n    }\n\n    handle(commands: Command[]): { commands: Command[]; onFinish: () => void } {\n        const subscription = fromSharedEvent(this.outputStream, 'error').subscribe(() => {\n            commands.forEach((command) => command.kill());\n\n            // Avoid further commands from spawning, e.g. if `RestartProcess` is used.\n            this.abortController.abort();\n        });\n\n        return {\n            commands,\n            onFinish: () => subscription.unsubscribe(),\n        };\n    }\n}\n"
  },
  {
    "path": "lib/flow-control/restart-process.spec.ts",
    "content": "import { VirtualTimeScheduler } from 'rxjs';\nimport { beforeEach, describe, expect, it, vi } from 'vitest';\n\nimport { createMockInstance } from '../__fixtures__/create-mock-instance.js';\nimport { createFakeCloseEvent, FakeCommand } from '../__fixtures__/fake-command.js';\nimport { Logger } from '../logger.js';\nimport { RestartProcess } from './restart-process.js';\n\nlet commands: FakeCommand[];\nlet controller: RestartProcess;\nlet logger: Logger;\nlet scheduler: VirtualTimeScheduler;\nbeforeEach(() => {\n    commands = [new FakeCommand(), new FakeCommand()];\n    logger = createMockInstance(Logger);\n\n    // Don't use TestScheduler as it's hardcoded to a max number of \"frames\" (time),\n    // which don't work for some tests in this suite\n    scheduler = new VirtualTimeScheduler();\n    controller = new RestartProcess({\n        logger,\n        scheduler,\n        delay: 100,\n        tries: 2,\n    });\n});\n\nit('does not restart processes that complete with success', () => {\n    controller.handle(commands);\n\n    commands[0].close.next(createFakeCloseEvent({ exitCode: 0 }));\n    commands[1].close.next(createFakeCloseEvent({ exitCode: 0 }));\n\n    scheduler.flush();\n\n    expect(commands[0].start).toHaveBeenCalledTimes(0);\n    expect(commands[1].start).toHaveBeenCalledTimes(0);\n});\n\nit('restarts processes that fail immediately, if no delay was passed', () => {\n    controller = new RestartProcess({ logger, scheduler, tries: 1 });\n    controller.handle(commands);\n\n    commands[0].close.next(createFakeCloseEvent({ exitCode: 1 }));\n    scheduler.flush();\n\n    expect(scheduler.now()).toBe(0);\n    expect(logger.logCommandEvent).toHaveBeenCalledExactlyOnceWith(\n        `${commands[0].command} restarted`,\n        commands[0],\n    );\n    expect(commands[0].start).toHaveBeenCalledTimes(1);\n});\n\nit('restarts processes that fail after delay ms has passed', () => {\n    controller.handle(commands);\n\n    commands[0].close.next(createFakeCloseEvent({ exitCode: 1 }));\n    commands[1].close.next(createFakeCloseEvent({ exitCode: 0 }));\n\n    scheduler.flush();\n\n    expect(scheduler.now()).toBe(100);\n    expect(logger.logCommandEvent).toHaveBeenCalledExactlyOnceWith(\n        `${commands[0].command} restarted`,\n        commands[0],\n    );\n    expect(commands[0].start).toHaveBeenCalledTimes(1);\n    expect(commands[1].start).not.toHaveBeenCalled();\n});\n\nit('restarts processes that fail with an exponential back-off', () => {\n    const tries = 4;\n    controller = new RestartProcess({ logger, scheduler, tries, delay: 'exponential' });\n    controller.handle(commands);\n\n    let time = 0;\n    for (let i = 0; i < tries; i++) {\n        commands[0].close.next(createFakeCloseEvent({ exitCode: 1 }));\n        scheduler.flush();\n\n        time += 2 ** i * 1000;\n        expect(scheduler.now()).toBe(time);\n        expect(logger.logCommandEvent).toHaveBeenCalledTimes(i + 1);\n        expect(commands[0].start).toHaveBeenCalledTimes(i + 1);\n    }\n});\n\nit('restarts processes up to tries', () => {\n    controller.handle(commands);\n\n    commands[0].close.next(createFakeCloseEvent({ exitCode: 1 }));\n    commands[0].close.next(createFakeCloseEvent({ exitCode: 'SIGTERM' }));\n    commands[0].close.next(createFakeCloseEvent({ exitCode: 'SIGTERM' }));\n    commands[1].close.next(createFakeCloseEvent({ exitCode: 0 }));\n\n    scheduler.flush();\n\n    expect(logger.logCommandEvent).toHaveBeenCalledTimes(2);\n    expect(logger.logCommandEvent).toHaveBeenCalledWith(\n        `${commands[0].command} restarted`,\n        commands[0],\n    );\n    expect(commands[0].start).toHaveBeenCalledTimes(2);\n});\n\nit('restart processes forever, if tries is negative', () => {\n    controller = new RestartProcess({\n        logger,\n        scheduler,\n        delay: 100,\n        tries: -1,\n    });\n    expect(controller.tries).toBe(Infinity);\n});\n\nit('restarts processes until they succeed', () => {\n    controller.handle(commands);\n\n    commands[0].close.next(createFakeCloseEvent({ exitCode: 1 }));\n    commands[0].close.next(createFakeCloseEvent({ exitCode: 0 }));\n    commands[1].close.next(createFakeCloseEvent({ exitCode: 0 }));\n\n    scheduler.flush();\n\n    expect(logger.logCommandEvent).toHaveBeenCalledExactlyOnceWith(\n        `${commands[0].command} restarted`,\n        commands[0],\n    );\n    expect(commands[0].start).toHaveBeenCalledTimes(1);\n});\n\ndescribe('returned commands', () => {\n    it('are the same if 0 tries are to be attempted', () => {\n        controller = new RestartProcess({ logger, scheduler });\n        expect(controller.handle(commands)).toMatchObject({ commands });\n    });\n\n    it('are not the same, but with same length if 1+ tries are to be attempted', () => {\n        const { commands: newCommands } = controller.handle(commands);\n        expect(newCommands).not.toBe(commands);\n        expect(newCommands).toHaveLength(commands.length);\n    });\n\n    it('skip close events followed by restarts', () => {\n        const { commands: newCommands } = controller.handle(commands);\n\n        const callback = vi.fn();\n        newCommands[0].close.subscribe(callback);\n        newCommands[1].close.subscribe(callback);\n\n        commands[0].close.next(createFakeCloseEvent({ exitCode: 1 }));\n        commands[0].close.next(createFakeCloseEvent({ exitCode: 1 }));\n        commands[0].close.next(createFakeCloseEvent({ exitCode: 1 }));\n        commands[1].close.next(createFakeCloseEvent({ exitCode: 1 }));\n        commands[1].close.next(createFakeCloseEvent({ exitCode: 0 }));\n\n        scheduler.flush();\n\n        // 1 failure from commands[0], 1 success from commands[1]\n        expect(callback).toHaveBeenCalledTimes(2);\n    });\n\n    it('keep non-close streams from original commands', () => {\n        const { commands: newCommands } = controller.handle(commands);\n        newCommands.forEach((newCommand, i) => {\n            expect(newCommand.close).not.toBe(commands[i].close);\n            expect(newCommand.error).toBe(commands[i].error);\n            expect(newCommand.stdout).toBe(commands[i].stdout);\n            expect(newCommand.stderr).toBe(commands[i].stderr);\n        });\n    });\n});\n"
  },
  {
    "path": "lib/flow-control/restart-process.ts",
    "content": "import Rx from 'rxjs';\nimport { defaultIfEmpty, delayWhen, filter, map, skip, take, takeWhile } from 'rxjs/operators';\n\nimport { Command } from '../command.js';\nimport * as defaults from '../defaults.js';\nimport { Logger } from '../logger.js';\nimport { FlowController } from './flow-controller.js';\n\nexport type RestartDelay = number | 'exponential';\n\n/**\n * Restarts commands that fail up to a defined number of times.\n */\nexport class RestartProcess implements FlowController {\n    private readonly logger: Logger;\n    private readonly scheduler?: Rx.SchedulerLike;\n    private readonly delay: RestartDelay;\n    readonly tries: number;\n\n    constructor({\n        delay,\n        tries,\n        logger,\n        scheduler,\n    }: {\n        delay?: RestartDelay;\n        tries?: number;\n        logger: Logger;\n        scheduler?: Rx.SchedulerLike;\n    }) {\n        this.logger = logger;\n        this.delay = delay ?? 0;\n        this.tries = tries != null ? +tries : defaults.restartTries;\n        this.tries = this.tries < 0 ? Infinity : this.tries;\n        this.scheduler = scheduler;\n    }\n\n    handle(commands: Command[]) {\n        if (this.tries === 0) {\n            return { commands };\n        }\n\n        const delayOperator = delayWhen((_, index) => {\n            const { delay } = this;\n            const value = delay === 'exponential' ? 2 ** index * 1000 : delay;\n            return Rx.timer(value, this.scheduler);\n        });\n\n        commands\n            .map((command) =>\n                command.close.pipe(\n                    take(this.tries),\n                    takeWhile(({ exitCode }) => exitCode !== 0),\n                ),\n            )\n            .forEach((failure, index) =>\n                Rx.merge(\n                    // Delay the emission (so that the restarts happen on time),\n                    // explicitly telling the subscriber that a restart is needed\n                    failure.pipe(\n                        delayOperator,\n                        map(() => true),\n                    ),\n                    // Skip the first N emissions (as these would be duplicates of the above),\n                    // meaning it will be empty because of success, or failed all N times,\n                    // and no more restarts should be attempted.\n                    failure.pipe(\n                        skip(this.tries),\n                        map(() => false),\n                        defaultIfEmpty(false),\n                    ),\n                ).subscribe((restart) => {\n                    const command = commands[index];\n                    if (restart) {\n                        this.logger.logCommandEvent(`${command.command} restarted`, command);\n                        command.start();\n                    }\n                }),\n            );\n\n        return {\n            commands: commands.map((command) => {\n                const closeStream = command.close.pipe(\n                    filter(({ exitCode }, emission) => {\n                        // We let all success codes pass, and failures only after restarting won't happen again\n                        return exitCode === 0 || emission >= this.tries;\n                    }),\n                );\n\n                // Return a proxy so that mutations happen on the original Command object.\n                // If either `Object.assign()` or `Object.create()` were used, it'd be hard to\n                // reflect the mutations on Command objects referenced by previous flow controllers.\n                return new Proxy(command, {\n                    get(target, prop: keyof Command) {\n                        return prop === 'close' ? closeStream : target[prop];\n                    },\n                });\n            }),\n        };\n    }\n}\n"
  },
  {
    "path": "lib/flow-control/teardown.spec.ts",
    "content": "import { ChildProcess } from 'node:child_process';\n\nimport { afterEach, describe, expect, it, Mock, vi } from 'vitest';\n\nimport { createMockInstance } from '../__fixtures__/create-mock-instance.js';\nimport { createFakeProcess, FakeCommand } from '../__fixtures__/fake-command.js';\nimport { SpawnCommand } from '../command.js';\nimport { Logger } from '../logger.js';\nimport * as spawn from '../spawn.js';\nimport { Teardown } from './teardown.js';\n\nconst spySpawn = vi\n    .spyOn(spawn, 'spawn')\n    .mockImplementation(() => createFakeProcess(1) as ChildProcess) as Mock;\nconst logger = createMockInstance(Logger);\nconst commands = [new FakeCommand()];\nconst teardown = 'cowsay bye';\n\nafterEach(() => {\n    vi.clearAllMocks();\n});\n\nconst create = (teardown: string[], spawn?: SpawnCommand) =>\n    new Teardown({\n        spawn,\n        logger,\n        commands: teardown,\n    });\n\nit('returns commands unchanged', () => {\n    const { commands: actual } = create([]).handle(commands);\n    expect(actual).toBe(commands);\n});\n\ndescribe('onFinish callback', () => {\n    it('does not spawn nothing if there are no teardown commands', () => {\n        create([]).handle(commands).onFinish();\n        expect(spySpawn).not.toHaveBeenCalled();\n    });\n\n    it('runs teardown command', () => {\n        create([teardown]).handle(commands).onFinish();\n        expect(spySpawn).toHaveBeenCalledWith(teardown, spawn.getSpawnOpts({ stdio: 'raw' }));\n    });\n\n    it('runs teardown command with custom spawn function', () => {\n        const customSpawn = vi.fn(() => createFakeProcess(1));\n        create([teardown], customSpawn).handle(commands).onFinish();\n        expect(customSpawn).toHaveBeenCalledWith(teardown, spawn.getSpawnOpts({ stdio: 'raw' }));\n    });\n\n    it('waits for teardown command to close', async () => {\n        const child = createFakeProcess(1);\n        spySpawn.mockReturnValue(child);\n\n        const result = create([teardown]).handle(commands).onFinish();\n        child.emit('close', 1, null);\n        await expect(result).resolves.toBeUndefined();\n    });\n\n    it('rejects if teardown command errors (string)', async () => {\n        const child = createFakeProcess(1);\n        spySpawn.mockReturnValue(child);\n\n        const result = create([teardown]).handle(commands).onFinish();\n        const error = 'fail';\n        child.emit('error', error);\n        await expect(result).rejects.toBe(error);\n        expect(logger.logGlobalEvent).toHaveBeenLastCalledWith('fail');\n    });\n\n    it('rejects if teardown command errors (error)', async () => {\n        const child = createFakeProcess(1);\n        spySpawn.mockReturnValue(child);\n\n        const result = create([teardown]).handle(commands).onFinish();\n        const error = new Error('fail');\n        child.emit('error', error);\n        await expect(result).rejects.toBe(error);\n        expect(logger.logGlobalEvent).toHaveBeenLastCalledWith(\n            expect.stringMatching(/Error: fail/),\n        );\n    });\n\n    it('rejects if teardown command errors (error, no stack)', async () => {\n        const child = createFakeProcess(1);\n        spySpawn.mockReturnValue(child);\n\n        const result = create([teardown]).handle(commands).onFinish();\n        const error = new Error('fail');\n        delete error.stack;\n        child.emit('error', error);\n        await expect(result).rejects.toBe(error);\n        expect(logger.logGlobalEvent).toHaveBeenLastCalledWith('Error: fail');\n    });\n\n    it('runs multiple teardown commands in sequence', async () => {\n        const child1 = createFakeProcess(1);\n        const child2 = createFakeProcess(2);\n        spySpawn.mockReturnValueOnce(child1).mockReturnValueOnce(child2);\n\n        const result = create(['foo', 'bar']).handle(commands).onFinish();\n\n        expect(spySpawn).toHaveBeenCalledTimes(1);\n        expect(spySpawn).toHaveBeenLastCalledWith('foo', spawn.getSpawnOpts({ stdio: 'raw' }));\n\n        child1.emit('close', 1, null);\n        await new Promise((resolve) => setTimeout(resolve));\n\n        expect(spySpawn).toHaveBeenCalledTimes(2);\n        expect(spySpawn).toHaveBeenLastCalledWith('bar', spawn.getSpawnOpts({ stdio: 'raw' }));\n\n        child2.emit('close', 0, null);\n        await expect(result).resolves.toBeUndefined();\n    });\n\n    it('stops running teardown commands on SIGINT', async () => {\n        const child = createFakeProcess(1);\n        spySpawn.mockReturnValue(child);\n\n        const result = create(['foo', 'bar']).handle(commands).onFinish();\n        child.emit('close', null, 'SIGINT');\n        await result;\n\n        expect(spySpawn).toHaveBeenCalledTimes(1);\n        expect(spySpawn).toHaveBeenLastCalledWith('foo', expect.anything());\n    });\n});\n"
  },
  {
    "path": "lib/flow-control/teardown.ts",
    "content": "import Rx from 'rxjs';\n\nimport { Command, SpawnCommand } from '../command.js';\nimport { Logger } from '../logger.js';\nimport { getSpawnOpts, spawn as baseSpawn } from '../spawn.js';\nimport { FlowController } from './flow-controller.js';\n\nexport class Teardown implements FlowController {\n    private readonly logger: Logger;\n    private readonly spawn: SpawnCommand;\n    private readonly teardown: readonly string[];\n\n    constructor({\n        logger,\n        spawn,\n        commands,\n    }: {\n        logger: Logger;\n        /**\n         * Which function to use to spawn commands.\n         * Defaults to the same used by the rest of concurrently.\n         */\n        spawn?: SpawnCommand;\n        commands: readonly string[];\n    }) {\n        this.logger = logger;\n        this.spawn = spawn || baseSpawn;\n        this.teardown = commands;\n    }\n\n    handle(commands: Command[]): { commands: Command[]; onFinish: () => Promise<void> } {\n        const { logger, teardown, spawn } = this;\n        const onFinish = async () => {\n            if (!teardown.length) {\n                return;\n            }\n\n            for (const command of teardown) {\n                logger.logGlobalEvent(`Running teardown command \"${command}\"`);\n\n                const child = spawn(command, getSpawnOpts({ stdio: 'raw' }));\n                const error = Rx.fromEvent(child, 'error');\n                const close = Rx.fromEvent(child, 'close');\n\n                try {\n                    const [exitCode, signal] = await Promise.race([\n                        Rx.firstValueFrom(error).then((event) => {\n                            throw event;\n                        }),\n                        Rx.firstValueFrom(close).then(\n                            (event) => event as [number | null, NodeJS.Signals | null],\n                        ),\n                    ]);\n\n                    logger.logGlobalEvent(\n                        `Teardown command \"${command}\" exited with code ${exitCode ?? signal}`,\n                    );\n\n                    if (signal === 'SIGINT') {\n                        break;\n                    }\n                } catch (error) {\n                    const errorText = String(error instanceof Error ? error.stack || error : error);\n                    logger.logGlobalEvent(`Teardown command \"${command}\" errored:`);\n                    logger.logGlobalEvent(errorText);\n                    return Promise.reject(error);\n                }\n            }\n        };\n\n        return { commands, onFinish };\n    }\n}\n"
  },
  {
    "path": "lib/index.ts",
    "content": "import process from 'node:process';\nimport { Readable } from 'node:stream';\n\nimport { assertDeprecated } from './assert.js';\nimport { CloseEvent, Command, CommandIdentifier, TimerEvent } from './command.js';\nimport {\n    concurrently as createConcurrently,\n    ConcurrentlyCommandInput,\n    ConcurrentlyOptions as BaseConcurrentlyOptions,\n    ConcurrentlyResult,\n} from './concurrently.js';\nimport type { FlowController } from './flow-control/flow-controller.js';\nimport { InputHandler } from './flow-control/input-handler.js';\nimport { KillOnSignal } from './flow-control/kill-on-signal.js';\nimport { KillOthers, ProcessCloseCondition } from './flow-control/kill-others.js';\nimport { LogError } from './flow-control/log-error.js';\nimport { LogExit } from './flow-control/log-exit.js';\nimport { LogOutput } from './flow-control/log-output.js';\nimport { LogTimings } from './flow-control/log-timings.js';\nimport { LoggerPadding } from './flow-control/logger-padding.js';\nimport { OutputErrorHandler } from './flow-control/output-error-handler.js';\nimport { RestartDelay, RestartProcess } from './flow-control/restart-process.js';\nimport { Teardown } from './flow-control/teardown.js';\nimport { Logger } from './logger.js';\nimport { castArray } from './utils.js';\n\nexport type ConcurrentlyOptions = Omit<BaseConcurrentlyOptions, 'abortSignal' | 'hide'> & {\n    // Logger options\n    /**\n     * Which command(s) should have their output hidden.\n     */\n    hide?: CommandIdentifier | CommandIdentifier[];\n\n    /**\n     * The prefix format to use when logging a command's output.\n     * Defaults to the command's index.\n     */\n    prefix?: string;\n\n    /**\n     * How many characters should a prefix have at most, used when the prefix format is `command`.\n     */\n    prefixLength?: number;\n\n    /**\n     * Pads short prefixes with spaces so that all prefixes have the same length.\n     */\n    padPrefix?: boolean;\n\n    /**\n     * Whether output should be formatted to include prefixes and whether \"event\" logs will be logged.\n     */\n    raw?: boolean;\n\n    /**\n     * Date format used when logging date/time.\n     * @see https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table\n     */\n    timestampFormat?: string;\n\n    // Input handling options\n    defaultInputTarget?: CommandIdentifier;\n    inputStream?: Readable;\n    handleInput?: boolean;\n    pauseInputStreamOnFinish?: boolean;\n\n    // Restarting options\n    /**\n     * How much time in milliseconds to wait before restarting a command.\n     *\n     * @see RestartProcess\n     */\n    restartDelay?: RestartDelay;\n\n    /**\n     * How many times commands should be restarted when they exit with a failure.\n     *\n     * @see RestartProcess\n     */\n    restartTries?: number;\n\n    // Process killing options\n    /**\n     * @deprecated Use `killOthersOn` instead.\n     * @see KillOthers\n     */\n    killOthers?: ProcessCloseCondition | ProcessCloseCondition[];\n    /**\n     * Once the first command exits with one of these statuses, kill other commands.\n     * @see KillOthers\n     */\n    killOthersOn?: ProcessCloseCondition | ProcessCloseCondition[];\n\n    /**\n     * Signal to send to killed processes.\n     */\n    killSignal?: string;\n\n    /**\n     * How many milliseconds to wait before killing processes.\n     */\n    killTimeout?: number;\n\n    // Timing options\n    /**\n     * Whether to output timing information for processes.\n     *\n     * @see LogTimings\n     */\n    timings?: boolean;\n\n    /**\n     * Clean up command(s) to execute before exiting concurrently.\n     * These won't be prefixed and don't affect concurrently's exit code.\n     */\n    teardown?: readonly string[];\n\n    /**\n     * List of additional arguments passed that will get replaced in each command.\n     * If not defined, no argument replacing will happen.\n     */\n    additionalArguments?: string[];\n};\n\nexport function concurrently(\n    commands: ConcurrentlyCommandInput[],\n    options: Partial<ConcurrentlyOptions> = {},\n) {\n    assertDeprecated(options.killOthers === undefined, 'killOthers', 'Use killOthersOn instead.');\n\n    // To avoid empty strings from hiding the output of commands that don't have a name,\n    // keep in the list of commands to hide only strings with some length.\n    // This might happen through the CLI when no `--hide` argument is specified, for example.\n    const hide = castArray(options.hide).filter((id) => id || id === 0);\n    const logger =\n        options.logger ||\n        new Logger({\n            hide,\n            prefixFormat: options.prefix,\n            commandLength: options.prefixLength,\n            raw: options.raw,\n            timestampFormat: options.timestampFormat,\n        });\n\n    if (options.prefixColors === false) {\n        logger.toggleColors(false);\n    }\n\n    const abortController = new AbortController();\n    const outputStream = options.outputStream || process.stdout;\n\n    return createConcurrently(commands, {\n        maxProcesses: options.maxProcesses,\n        raw: options.raw,\n        successCondition: options.successCondition,\n        cwd: options.cwd,\n        hide,\n        logger,\n        outputStream,\n        group: options.group,\n        abortSignal: abortController.signal,\n        controllers: [\n            // LoggerPadding needs to run before any other controllers that might output something\n            ...(options.padPrefix ? [new LoggerPadding({ logger })] : []),\n            new LogError({ logger }),\n            new LogOutput({ logger }),\n            new LogExit({ logger }),\n            new InputHandler({\n                logger,\n                defaultInputTarget: options.defaultInputTarget,\n                inputStream:\n                    options.inputStream || (options.handleInput ? process.stdin : undefined),\n                pauseInputStreamOnFinish: options.pauseInputStreamOnFinish,\n            }),\n            new KillOnSignal({ process, abortController }),\n            new RestartProcess({\n                logger,\n                delay: options.restartDelay,\n                tries: options.restartTries,\n            }),\n            new KillOthers({\n                logger,\n                conditions: options.killOthersOn || options.killOthers || [],\n                timeoutMs: options.killTimeout,\n                killSignal: options.killSignal,\n                abortController,\n            }),\n            new OutputErrorHandler({ abortController, outputStream }),\n            new LogTimings({\n                logger: options.timings ? logger : undefined,\n                timestampFormat: options.timestampFormat,\n            }),\n            new Teardown({ logger, spawn: options.spawn, commands: options.teardown || [] }),\n        ],\n        prefixColors: options.prefixColors || [],\n        additionalArguments: options.additionalArguments,\n    });\n}\n\n// Export all flow controllers, types, and the main concurrently function,\n// so that 3rd-parties can use them however they want\n\n// Main\nexport default concurrently;\nexport { ConcurrentlyCommandInput, ConcurrentlyResult, createConcurrently, Logger };\n\n// Command specific\nexport { CloseEvent, Command, CommandIdentifier, TimerEvent };\n\n// Flow controllers\nexport {\n    FlowController,\n    InputHandler,\n    KillOnSignal,\n    KillOthers,\n    LogError,\n    LogExit,\n    LogOutput,\n    LogTimings,\n    RestartProcess,\n};\n"
  },
  {
    "path": "lib/jsonc.spec.ts",
    "content": "/*\nORIGINAL https://www.npmjs.com/package/tiny-jsonc\nBY Fabio Spampinato\nMIT license\n\nCopied due to the dependency not being compatible with CommonJS\n*/\n\nimport { expect, it } from 'vitest';\n\nimport JSONC from './jsonc.js';\n\nconst fixtures = {\n    errors: {\n        comment: '// asd',\n        empty: '',\n        prefix: 'invalid 123',\n        suffix: '123 invalid',\n        multiLineString: `\n        {\n            \"foo\": \"/*\n            */\"\n        }\n        `,\n    },\n    parse: {\n        input: `\n        // Example // Yes\n        /* EXAMPLE */ /* YES */\n        {\n            \"one\": {},\n            \"two\" :{},\n            \"three\": {\n                \"one\": null,\n                \"two\" :true,\n                \"three\": false,\n                \"four\": \"asd\\\\n\\\\u0022\\\\\"\",\n                \"five\": -123.123e10,\n                \"six\": [ 123, true, [],],\n            },\n        }\n        // Example // Yes\n        /* EXAMPLE */ /* YES */\n        `,\n        output: {\n            one: {},\n            two: {},\n            three: {\n                one: null,\n                two: true,\n                three: false,\n                four: 'asd\\n\\u0022\"',\n                five: -123.123e10,\n                six: [123, true, []],\n            },\n        },\n    },\n};\n\nit('supports strings with comments and trailing commas', () => {\n    const { input, output } = fixtures.parse;\n\n    expect(JSONC.parse(input)).toEqual(output);\n});\n\nit('throws on invalid input', () => {\n    const { prefix, suffix } = fixtures.errors;\n\n    expect(() => JSONC.parse(prefix)).toThrow(SyntaxError);\n    expect(() => JSONC.parse(suffix)).toThrow(SyntaxError);\n});\n\nit('throws on insufficient input', () => {\n    const { comment, empty } = fixtures.errors;\n\n    expect(() => JSONC.parse(comment)).toThrow(SyntaxError);\n    expect(() => JSONC.parse(empty)).toThrow(SyntaxError);\n});\n\nit('throws on multi-line strings', () => {\n    const { multiLineString } = fixtures.errors;\n\n    expect(() => JSONC.parse(multiLineString)).toThrow(SyntaxError);\n});\n"
  },
  {
    "path": "lib/jsonc.ts",
    "content": "/*\nORIGINAL https://www.npmjs.com/package/tiny-jsonc\nBY Fabio Spampinato\nMIT license\n\nCopied due to the dependency not being compatible with CommonJS\n*/\n\nconst stringOrCommentRe = /(\"(?:\\\\?[\\s\\S])*?\")|(\\/\\/.*)|(\\/\\*[\\s\\S]*?\\*\\/)/g;\nconst stringOrTrailingCommaRe = /(\"(?:\\\\?[\\s\\S])*?\")|(,\\s*)(?=\\]|\\})/g;\n\nconst JSONC = {\n    parse: (text: string) => {\n        text = String(text); // To be extra safe\n\n        try {\n            // Fast path for valid JSON\n            return JSON.parse(text);\n        } catch {\n            // Slow path for JSONC and invalid inputs\n            return JSON.parse(\n                text.replace(stringOrCommentRe, '$1').replace(stringOrTrailingCommaRe, '$1'),\n            );\n        }\n    },\n    stringify: JSON.stringify,\n};\n\nexport default JSONC;\n"
  },
  {
    "path": "lib/logger.spec.ts",
    "content": "import { subscribeSpyTo } from '@hirez_io/observer-spy';\nimport chalk from 'chalk';\nimport { beforeEach, describe, expect, it, vi } from 'vitest';\n\nimport { FakeCommand } from './__fixtures__/fake-command.js';\nimport { Logger } from './logger.js';\n\nbeforeEach(() => {\n    // Force Chalk to use colors, otherwise tests may pass when they were supposed to be failing.\n    chalk.level = 3;\n});\n\nconst createLogger = (...options: ConstructorParameters<typeof Logger>) => {\n    const logger = new Logger(...options);\n    vi.spyOn(logger, 'log');\n    const spy = subscribeSpyTo(logger.output);\n    return { logger, spy };\n};\n\ndescribe('#log()', () => {\n    it('emits prefix + text in the output stream', () => {\n        const { logger, spy } = createLogger({});\n        logger.log('foo', 'bar');\n\n        const values = spy.getValues();\n        expect(values).toHaveLength(2);\n        expect(values[0]).toEqual({ command: undefined, text: 'foo' });\n        expect(values[1]).toEqual({ command: undefined, text: 'bar' });\n    });\n\n    it('emits multiple lines of text with prefix on each', () => {\n        const { logger, spy } = createLogger({});\n        logger.log('foo', 'bar\\nbaz\\n');\n\n        const values = spy.getValues();\n        expect(values).toHaveLength(2);\n        expect(values[0]).toEqual({ command: undefined, text: 'foo' });\n        expect(values[1]).toEqual({ command: undefined, text: 'bar\\nfoobaz\\n' });\n    });\n\n    it('does not emit prefix if previous call from same command did not finish with a LF', () => {\n        const { logger, spy } = createLogger({});\n        const command = new FakeCommand();\n        logger.log('foo', 'bar', command);\n        logger.log('foo', 'baz', command);\n\n        expect(spy.getValuesLength()).toBe(3);\n        expect(spy.getLastValue()).toEqual({ command, text: 'baz' });\n    });\n\n    it('emits LF and prefix if previous call is from different command and did not finish with a LF', () => {\n        const { logger, spy } = createLogger({});\n        const command1 = new FakeCommand();\n        logger.log('foo', 'bar', command1);\n\n        const command2 = new FakeCommand();\n        logger.log('foo', 'baz', command2);\n\n        const values = spy.getValues();\n        expect(values).toHaveLength(5);\n        expect(values).toContainEqual({ command: command1, text: '\\n' });\n        expect(values).toContainEqual({ command: command2, text: 'foo' });\n        expect(values).toContainEqual({ command: command2, text: 'baz' });\n    });\n\n    it('does not emit prefix nor handle text if logger is in raw mode', () => {\n        const { logger, spy } = createLogger({ raw: true });\n        logger.log('foo', 'bar\\nbaz\\n');\n\n        const values = spy.getValues();\n        expect(values).toHaveLength(1);\n        expect(values[0]).toEqual({ command: undefined, text: 'bar\\nbaz\\n' });\n    });\n});\n\ndescribe('#logGlobalEvent()', () => {\n    it('does nothing if in raw mode', () => {\n        const { logger } = createLogger({ raw: true });\n        logger.logGlobalEvent('foo');\n\n        expect(logger.log).not.toHaveBeenCalled();\n    });\n\n    it('logs in gray dim style with arrow prefix', () => {\n        const { logger } = createLogger({});\n        logger.logGlobalEvent('foo');\n\n        expect(logger.log).toHaveBeenCalledWith(\n            `${chalk.reset('-->')} `,\n            `${chalk.reset('foo')}\\n`,\n        );\n    });\n});\n\ndescribe('#logCommandText()', () => {\n    it('logs with name if no prefixFormat is set', () => {\n        const { logger } = createLogger({});\n        const cmd = new FakeCommand('bla');\n        logger.logCommandText('foo', cmd);\n\n        expect(logger.log).toHaveBeenCalledWith(`${chalk.reset('[bla]')} `, 'foo', cmd);\n    });\n\n    it('logs with index if no prefixFormat is set, and command has no name', () => {\n        const { logger } = createLogger({});\n        const cmd = new FakeCommand('', undefined, 2);\n        logger.logCommandText('foo', cmd);\n\n        expect(logger.log).toHaveBeenCalledWith(`${chalk.reset('[2]')} `, 'foo', cmd);\n    });\n\n    it('logs with prefixFormat set to pid', () => {\n        const { logger } = createLogger({ prefixFormat: 'pid' });\n        const cmd = new FakeCommand();\n        cmd.pid = 123;\n        logger.logCommandText('foo', cmd);\n\n        expect(logger.log).toHaveBeenCalledWith(`${chalk.reset('[123]')} `, 'foo', cmd);\n    });\n\n    it('logs with prefixFormat set to name', () => {\n        const { logger } = createLogger({ prefixFormat: 'name' });\n        const cmd = new FakeCommand('bar');\n        logger.logCommandText('foo', cmd);\n\n        expect(logger.log).toHaveBeenCalledWith(`${chalk.reset('[bar]')} `, 'foo', cmd);\n    });\n\n    it('logs with prefixFormat set to index', () => {\n        const { logger } = createLogger({ prefixFormat: 'index' });\n        const cmd = new FakeCommand(undefined, undefined, 3);\n        logger.logCommandText('foo', cmd);\n\n        expect(logger.log).toHaveBeenCalledWith(`${chalk.reset('[3]')} `, 'foo', cmd);\n    });\n\n    it('logs with prefixFormat set to time (with timestampFormat)', () => {\n        const { logger } = createLogger({ prefixFormat: 'time', timestampFormat: 'yyyy' });\n        const cmd = new FakeCommand();\n        logger.logCommandText('foo', cmd);\n\n        const year = new Date().getFullYear();\n        expect(logger.log).toHaveBeenCalledWith(`${chalk.reset(`[${year}]`)} `, 'foo', cmd);\n    });\n\n    it('logs with templated prefixFormat', () => {\n        const { logger } = createLogger({ prefixFormat: '{index}-{name}' });\n        const cmd = new FakeCommand('bar');\n        logger.logCommandText('foo', cmd);\n\n        expect(logger.log).toHaveBeenCalledWith(`${chalk.reset('0-bar')} `, 'foo', cmd);\n    });\n\n    it('does not strip spaces from beginning or end of prefixFormat', () => {\n        const { logger } = createLogger({ prefixFormat: ' {index}-{name} ' });\n        const cmd = new FakeCommand('bar');\n        logger.logCommandText('foo', cmd);\n\n        expect(logger.log).toHaveBeenCalledWith(`${chalk.reset(' 0-bar ')} `, 'foo', cmd);\n    });\n\n    it('logs with no prefix', () => {\n        const { logger } = createLogger({ prefixFormat: 'none' });\n        const cmd = new FakeCommand();\n        logger.logCommandText('foo', cmd);\n\n        expect(logger.log).toHaveBeenCalledWith(chalk.reset(''), 'foo', cmd);\n    });\n\n    it('logs prefix using command line itself', () => {\n        const { logger } = createLogger({ prefixFormat: 'command' });\n        const cmd = new FakeCommand();\n        logger.logCommandText('foo', cmd);\n\n        expect(logger.log).toHaveBeenCalledWith(`${chalk.reset('[echo foo]')} `, 'foo', cmd);\n    });\n\n    it('logs prefix using command line itself, capped at commandLength bytes', () => {\n        const { logger } = createLogger({ prefixFormat: 'command', commandLength: 6 });\n        const cmd = new FakeCommand();\n        logger.logCommandText('foo', cmd);\n\n        expect(logger.log).toHaveBeenCalledWith(`${chalk.reset('[ec..oo]')} `, 'foo', cmd);\n    });\n\n    it('logs default prefixes with padding', () => {\n        const { logger } = createLogger({});\n        const cmd = new FakeCommand('foo');\n        logger.setPrefixLength(5);\n        logger.logCommandText('bar', cmd);\n\n        expect(logger.log).toHaveBeenCalledWith(`${chalk.reset('[foo  ]')} `, 'bar', cmd);\n    });\n\n    it('logs templated prefixes with padding', () => {\n        const { logger } = createLogger({ prefixFormat: '{name}-{index}' });\n        const cmd = new FakeCommand('foo', undefined, 0);\n        logger.setPrefixLength(6);\n        logger.logCommandText('bar', cmd);\n\n        expect(logger.log).toHaveBeenCalledWith(`${chalk.reset('foo-0 ')} `, 'bar', cmd);\n    });\n\n    it('logs prefix using prefixColor from command', () => {\n        const { logger } = createLogger({});\n        const cmd = new FakeCommand('', undefined, 1, {\n            prefixColor: 'blue',\n        });\n        logger.logCommandText('foo', cmd);\n\n        expect(logger.log).toHaveBeenCalledWith(`${chalk.blue('[1]')} `, 'foo', cmd);\n    });\n\n    it('logs prefix using default color if prefixColor from command is not a valid color', () => {\n        const { logger } = createLogger({});\n        const cmd = new FakeCommand('', undefined, 1, {\n            prefixColor: 'fake.bold',\n        });\n        logger.logCommandText('foo', cmd);\n\n        expect(logger.log).toHaveBeenCalledWith(chalk.reset('[1]') + ' ', 'foo', cmd);\n    });\n\n    it('logs prefix in gray dim if prefixColor from command does not exist', () => {\n        const { logger } = createLogger({});\n        const cmd = new FakeCommand('', undefined, 1, {\n            prefixColor: 'blue.fake',\n        });\n        logger.logCommandText('foo', cmd);\n\n        expect(logger.log).toHaveBeenCalledWith(`${chalk.reset('[1]')} `, 'foo', cmd);\n    });\n\n    it('logs prefix using prefixColor from command if prefixColor is a hex value', () => {\n        const { logger } = createLogger({});\n        const prefixColor = '#32bd8a';\n        const cmd = new FakeCommand('', undefined, 1, {\n            prefixColor,\n        });\n        logger.logCommandText('foo', cmd);\n\n        expect(logger.log).toHaveBeenCalledWith(`${chalk.hex(prefixColor)('[1]')} `, 'foo', cmd);\n    });\n\n    it('logs prefix using prefixColor from command if prefixColor is a hex value with modifiers', () => {\n        const { logger } = createLogger({});\n        const prefixColor = '#32bd8a.inverse';\n        const cmd = new FakeCommand('', undefined, 1, {\n            prefixColor,\n        });\n        logger.logCommandText('foo', cmd);\n\n        expect(logger.log).toHaveBeenCalledWith(\n            `${chalk.hex(prefixColor).inverse('[1]')} `,\n            'foo',\n            cmd,\n        );\n    });\n\n    it('does nothing if command is hidden by name', () => {\n        const { logger } = createLogger({ hide: ['abc'] });\n        const cmd = new FakeCommand('abc');\n        logger.logCommandText('foo', cmd);\n\n        expect(logger.log).not.toHaveBeenCalled();\n    });\n\n    it('does nothing if command is hidden by index', () => {\n        const { logger } = createLogger({ hide: [3] });\n        const cmd = new FakeCommand('', undefined, 3);\n        logger.logCommandText('foo', cmd);\n\n        expect(logger.log).not.toHaveBeenCalled();\n    });\n});\n\ndescribe('#logCommandEvent()', () => {\n    it('does nothing if in raw mode', () => {\n        const { logger } = createLogger({ raw: true });\n        logger.logCommandEvent('foo', new FakeCommand());\n\n        expect(logger.log).not.toHaveBeenCalled();\n    });\n\n    it('does nothing if command is hidden by name', () => {\n        const { logger } = createLogger({ hide: ['abc'] });\n        const cmd = new FakeCommand('abc');\n        logger.logCommandEvent('foo', cmd);\n\n        expect(logger.log).not.toHaveBeenCalled();\n    });\n\n    it('does nothing if command is hidden by index', () => {\n        const { logger } = createLogger({ hide: [3] });\n        const cmd = new FakeCommand('', undefined, 3);\n        logger.logCommandEvent('foo', cmd);\n\n        expect(logger.log).not.toHaveBeenCalled();\n    });\n\n    it('logs text in gray dim', () => {\n        const { logger } = createLogger({});\n        const cmd = new FakeCommand('', undefined, 1);\n        logger.logCommandEvent('foo', cmd);\n\n        expect(logger.log).toHaveBeenCalledWith(\n            `${chalk.reset('[1]')} `,\n            `${chalk.reset('foo')}\\n`,\n            cmd,\n        );\n    });\n\n    it('prepends a LF if previous command write did not end with a LF', () => {\n        const { logger } = createLogger({});\n        const cmd = new FakeCommand('', undefined, 1);\n        logger.logCommandText('text', cmd);\n        logger.logCommandEvent('event', cmd);\n\n        expect(logger.log).toHaveBeenCalledWith(\n            `${chalk.reset('[1]')} `,\n            `\\n${chalk.reset('event')}\\n`,\n            cmd,\n        );\n    });\n});\n\ndescribe('#logTable()', () => {\n    it('does not log anything in raw mode', () => {\n        const { logger } = createLogger({ raw: true });\n        logger.logTable([{ foo: 1, bar: 2 }]);\n\n        expect(logger.log).not.toHaveBeenCalled();\n    });\n\n    it('does not log anything if value is not an array', () => {\n        const { logger } = createLogger({});\n        logger.logTable({} as never);\n        logger.logTable(null as never);\n        logger.logTable(0 as never);\n        logger.logTable('' as never);\n\n        expect(logger.log).not.toHaveBeenCalled();\n    });\n\n    it('does not log anything if array is empty', () => {\n        const { logger } = createLogger({});\n        logger.logTable([]);\n\n        expect(logger.log).not.toHaveBeenCalled();\n    });\n\n    it('does not log anything if array items have no properties', () => {\n        const { logger } = createLogger({});\n        logger.logTable([{}]);\n\n        expect(logger.log).not.toHaveBeenCalled();\n    });\n\n    it(\"logs a header for each item's properties\", () => {\n        const { logger } = createLogger({});\n        logger.logTable([{ foo: 1, bar: 2 }]);\n\n        expect(logger.log).toHaveBeenCalledWith(\n            `${chalk.reset('-->')} `,\n            `${chalk.reset('│ foo │ bar │')}\\n`,\n        );\n    });\n\n    it(\"logs padded headers according to longest column's value\", () => {\n        const { logger } = createLogger({});\n        logger.logTable([{ a: 'foo', b: 'barbaz' }]);\n\n        expect(logger.log).toHaveBeenCalledWith(\n            `${chalk.reset('-->')} `,\n            `${chalk.reset('│ a   │ b      │')}\\n`,\n        );\n    });\n\n    it(\"logs each items's values\", () => {\n        const { logger } = createLogger({});\n        logger.logTable([{ foo: 123 }, { foo: 456 }]);\n\n        expect(logger.log).toHaveBeenCalledWith(\n            `${chalk.reset('-->')} `,\n            `${chalk.reset('│ 123 │')}\\n`,\n        );\n        expect(logger.log).toHaveBeenCalledWith(\n            `${chalk.reset('-->')} `,\n            `${chalk.reset('│ 456 │')}\\n`,\n        );\n    });\n\n    it(\"logs each items's values with empty column\", () => {\n        const { logger } = createLogger({});\n        logger.logTable([{ foo: 123 }, { foo: null }]);\n\n        expect(logger.log).toHaveBeenCalledWith(\n            `${chalk.reset('-->')} `,\n            `${chalk.reset('│ 123 │')}\\n`,\n        );\n        expect(logger.log).toHaveBeenCalledWith(\n            `${chalk.reset('-->')} `,\n            `${chalk.reset('│     │')}\\n`,\n        );\n    });\n\n    it(\"logs each items's values padded according to longest column's value\", () => {\n        const { logger } = createLogger({});\n        logger.logTable([{ foo: 1 }, { foo: 123 }]);\n\n        expect(logger.log).toHaveBeenCalledWith(\n            `${chalk.reset('-->')} `,\n            `${chalk.reset('│ 1   │')}\\n`,\n        );\n    });\n\n    it('logs items with different properties in each', () => {\n        const { logger } = createLogger({});\n        logger.logTable([{ foo: 1 }, { bar: 2 }]);\n\n        expect(logger.log).toHaveBeenCalledWith(\n            `${chalk.reset('-->')} `,\n            `${chalk.reset('│ foo │ bar │')}\\n`,\n        );\n        expect(logger.log).toHaveBeenCalledWith(\n            `${chalk.reset('-->')} `,\n            `${chalk.reset('│ 1   │     │')}\\n`,\n        );\n        expect(logger.log).toHaveBeenCalledWith(\n            `${chalk.reset('-->')} `,\n            `${chalk.reset('│     │ 2   │')}\\n`,\n        );\n    });\n});\n\ndescribe('#toggleColors()', () => {\n    it('uses supported color level when on', () => {\n        const { logger, spy } = createLogger({});\n        logger.toggleColors(true);\n\n        const command1 = new FakeCommand('foo', 'command', 0, { prefixColor: 'red' });\n        logger.logCommandText('bar', command1);\n        logger.logGlobalEvent('baz');\n\n        const texts = spy.getValues().map((value) => value.text);\n        expect(texts).toContain(`${chalk.red('[foo]')} `);\n        expect(texts).toContain(`${chalk.reset('-->')} `);\n    });\n\n    it('uses no colors when off', () => {\n        const { logger, spy } = createLogger({});\n        logger.toggleColors(false);\n\n        const command1 = new FakeCommand('foo', 'command', 0, { prefixColor: 'red' });\n        logger.logCommandText('bar', command1);\n        logger.logGlobalEvent('baz');\n\n        const texts = spy.getValues().map((value) => value.text);\n        expect(texts).toContain('[foo] ');\n        expect(texts).toContain('--> ');\n    });\n});\n"
  },
  {
    "path": "lib/logger.ts",
    "content": "import chalk, { Chalk, ChalkInstance } from 'chalk';\nimport Rx from 'rxjs';\n\nimport { Command, CommandIdentifier } from './command.js';\nimport { DateFormatter } from './date-format.js';\nimport * as defaults from './defaults.js';\nimport { escapeRegExp } from './utils.js';\n\nconst defaultChalk = chalk;\nconst noColorChalk = new Chalk({ level: 0 });\n\nfunction getChalkPath(chalk: ChalkInstance, path: string): ChalkInstance | undefined {\n    return path\n        .split('.')\n        .reduce(\n            (prev, key) => prev && (prev as unknown as Record<string, ChalkInstance>)[key],\n            chalk,\n        );\n}\n\nexport class Logger {\n    private readonly hide: CommandIdentifier[];\n    private readonly raw: boolean;\n    private readonly prefixFormat?: string;\n    private readonly commandLength: number;\n    private readonly dateFormatter: DateFormatter;\n\n    private chalk = defaultChalk;\n\n    /**\n     * How many characters should a prefix have.\n     * Prefixes shorter than this will be padded with spaces to the right.\n     */\n    private prefixLength = 0;\n\n    /**\n     * Last character emitted, and from which command.\n     * If `undefined`, then nothing has been logged yet.\n     */\n    private lastWrite?: { command: Command | undefined; char: string };\n\n    /**\n     * Observable that emits when there's been output logged.\n     * If `command` is is `undefined`, then the log is for a global event.\n     */\n    readonly output = new Rx.Subject<{ command: Command | undefined; text: string }>();\n\n    constructor({\n        hide,\n        prefixFormat,\n        commandLength,\n        raw = false,\n        timestampFormat,\n    }: {\n        /**\n         * Which commands should have their output hidden.\n         */\n        hide?: CommandIdentifier[];\n\n        /**\n         * Whether output should be formatted to include prefixes and whether \"event\" logs will be\n         * logged.\n         */\n        raw?: boolean;\n\n        /**\n         * The prefix format to use when logging a command's output.\n         * Defaults to the command's index.\n         */\n        prefixFormat?: string;\n\n        /**\n         * How many characters should a prefix have at most when the format is `command`.\n         */\n        commandLength?: number;\n\n        /**\n         * Date format used when logging date/time.\n         * @see https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table\n         */\n        timestampFormat?: string;\n    }) {\n        this.hide = (hide || []).map(String);\n        this.raw = raw;\n        this.prefixFormat = prefixFormat;\n        this.commandLength = commandLength || defaults.prefixLength;\n        this.dateFormatter = new DateFormatter(timestampFormat || defaults.timestampFormat);\n    }\n\n    /**\n     * Toggles colors on/off globally.\n     */\n    toggleColors(on: boolean) {\n        this.chalk = on ? defaultChalk : noColorChalk;\n    }\n\n    private shortenText(text: string) {\n        if (!text || text.length <= this.commandLength) {\n            return text;\n        }\n\n        const ellipsis = '..';\n        const prefixLength = this.commandLength - ellipsis.length;\n        const endLength = Math.floor(prefixLength / 2);\n        const beginningLength = prefixLength - endLength;\n\n        const beginning = text.slice(0, beginningLength);\n        const end = text.slice(text.length - endLength, text.length);\n        return beginning + ellipsis + end;\n    }\n\n    private getPrefixesFor(command: Command): Record<string, string> {\n        return {\n            // When there's limited concurrency, the PID might not be immediately available,\n            // so avoid the string 'undefined' from becoming a prefix\n            pid: command.pid != null ? String(command.pid) : '',\n            index: String(command.index),\n            name: command.name,\n            command: this.shortenText(command.command),\n            time: this.dateFormatter.format(new Date()),\n        };\n    }\n\n    getPrefixContent(\n        command: Command,\n    ): { type: 'default' | 'template'; value: string } | undefined {\n        const prefix = this.prefixFormat || (command.name ? 'name' : 'index');\n        if (prefix === 'none') {\n            return;\n        }\n\n        const prefixes = this.getPrefixesFor(command);\n        if (Object.keys(prefixes).includes(prefix)) {\n            return { type: 'default', value: prefixes[prefix] };\n        }\n\n        const value = Object.entries(prefixes).reduce((prev, [key, val]) => {\n            const keyRegex = new RegExp(escapeRegExp(`{${key}}`), 'g');\n            return prev.replace(keyRegex, String(val));\n        }, prefix);\n        return { type: 'template', value };\n    }\n\n    getPrefix(command: Command): string {\n        const content = this.getPrefixContent(command);\n        if (!content) {\n            return '';\n        }\n\n        return content.type === 'template'\n            ? content.value.padEnd(this.prefixLength, ' ')\n            : `[${content.value.padEnd(this.prefixLength, ' ')}]`;\n    }\n\n    setPrefixLength(length: number) {\n        this.prefixLength = length;\n    }\n\n    colorText(command: Command, text: string) {\n        let color: ChalkInstance;\n        if (command.prefixColor?.startsWith('#')) {\n            const [hexColor, ...modifiers] = command.prefixColor.split('.');\n            color = this.chalk.hex(hexColor);\n            const modifiedColor = getChalkPath(color, modifiers.join('.'));\n            if (modifiedColor) {\n                color = modifiedColor;\n            }\n        } else {\n            const defaultColor = getChalkPath(this.chalk, defaults.prefixColors) as ChalkInstance;\n            color = getChalkPath(this.chalk, command.prefixColor ?? '') ?? defaultColor;\n        }\n        return color(text);\n    }\n\n    /**\n     * Logs an event for a command (e.g. start, stop).\n     *\n     * If raw mode is on, then nothing is logged.\n     */\n    logCommandEvent(text: string, command: Command) {\n        if (this.raw) {\n            return;\n        }\n\n        // Last write was from this command, but it didn't end with a line feed.\n        // Prepend one, otherwise the event's text will be concatenated to that write.\n        // A line feed is otherwise inserted anyway.\n        let prefix = '';\n        if (this.lastWrite?.command === command && this.lastWrite.char !== '\\n') {\n            prefix = '\\n';\n        }\n        this.logCommandText(`${prefix}${this.chalk.reset(text)}\\n`, command);\n    }\n\n    logCommandText(text: string, command: Command) {\n        if (this.hide.includes(String(command.index)) || this.hide.includes(command.name)) {\n            return;\n        }\n\n        const prefix = this.colorText(command, this.getPrefix(command));\n        return this.log(prefix + (prefix ? ' ' : ''), text, command);\n    }\n\n    /**\n     * Logs a global event (e.g. sending signals to processes).\n     *\n     * If raw mode is on, then nothing is logged.\n     */\n    logGlobalEvent(text: string) {\n        if (this.raw) {\n            return;\n        }\n\n        this.log(`${this.chalk.reset('-->')} `, `${this.chalk.reset(text)}\\n`);\n    }\n\n    /**\n     * Logs a table from an input object array, like `console.table`.\n     *\n     * Each row is a single input item, and they are presented in the input order.\n     */\n    logTable(tableContents: Record<string, unknown>[]) {\n        // For now, can only print array tables with some content.\n        if (this.raw || !Array.isArray(tableContents) || !tableContents.length) {\n            return;\n        }\n\n        let nextColIndex = 0;\n        const headers: Record<string, { index: number; length: number }> = {};\n        const contentRows = tableContents.map((row) => {\n            const rowContents: string[] = [];\n            Object.keys(row).forEach((col) => {\n                if (!headers[col]) {\n                    headers[col] = {\n                        index: nextColIndex++,\n                        length: col.length,\n                    };\n                }\n\n                const colIndex = headers[col].index;\n                const formattedValue = String(row[col] == null ? '' : row[col]);\n                // Update the column length in case this rows value is longer than the previous length for the column.\n                headers[col].length = Math.max(formattedValue.length, headers[col].length);\n                rowContents[colIndex] = formattedValue;\n                return rowContents;\n            });\n            return rowContents;\n        });\n\n        const headersFormatted = Object.keys(headers).map((header) =>\n            header.padEnd(headers[header].length, ' '),\n        );\n\n        if (!headersFormatted.length) {\n            // No columns exist.\n            return;\n        }\n\n        const borderRowFormatted = headersFormatted.map((header) => '─'.padEnd(header.length, '─'));\n\n        this.logGlobalEvent(`┌─${borderRowFormatted.join('─┬─')}─┐`);\n        this.logGlobalEvent(`│ ${headersFormatted.join(' │ ')} │`);\n        this.logGlobalEvent(`├─${borderRowFormatted.join('─┼─')}─┤`);\n\n        contentRows.forEach((contentRow) => {\n            const contentRowFormatted = headersFormatted.map((header, colIndex) => {\n                // If the table was expanded after this row was processed, it won't have this column.\n                // Use an empty string in this case.\n                const col = contentRow[colIndex] || '';\n                return col.padEnd(header.length, ' ');\n            });\n            this.logGlobalEvent(`│ ${contentRowFormatted.join(' │ ')} │`);\n        });\n\n        this.logGlobalEvent(`└─${borderRowFormatted.join('─┴─')}─┘`);\n    }\n\n    log(prefix: string, text: string, command?: Command) {\n        if (this.raw) {\n            return this.emit(command, text);\n        }\n\n        // #70 - replace some ANSI code that would impact clearing lines\n        text = text.replace(/\\u2026/g, '...');\n\n        // This write's interrupting another command, emit a line feed to start clean.\n        if (this.lastWrite && this.lastWrite.command !== command && this.lastWrite.char !== '\\n') {\n            this.emit(this.lastWrite.command, '\\n');\n        }\n\n        // Clean lines should emit a prefix\n        if (!this.lastWrite || this.lastWrite.char === '\\n') {\n            this.emit(command, prefix);\n        }\n\n        const textToWrite = text.replaceAll('\\n', (lf, i) => lf + (text[i + 1] ? prefix : ''));\n        this.emit(command, textToWrite);\n    }\n\n    emit(command: Command | undefined, text: string) {\n        this.lastWrite = { command, char: text[text.length - 1] };\n        this.output.next({ command, text });\n    }\n}\n"
  },
  {
    "path": "lib/observables.spec.ts",
    "content": "import EventEmitter from 'node:events';\n\nimport { describe, expect, it } from 'vitest';\n\nimport { fromSharedEvent } from './observables.js';\n\ndescribe('fromSharedEvent()', () => {\n    it('returns same observable for event emitter/name pair', () => {\n        const emitter = new EventEmitter();\n        const obs1 = fromSharedEvent(emitter, 'foo');\n        const obs2 = fromSharedEvent(emitter, 'foo');\n        expect(obs1).toBe(obs2);\n    });\n\n    it('returns different observables for different event emitter/name pairs', () => {\n        const emitter = new EventEmitter();\n        const obs1 = fromSharedEvent(emitter, 'foo');\n        const obs2 = fromSharedEvent(emitter, 'bar');\n        expect(obs1).not.toBe(obs2);\n\n        const emitter2 = new EventEmitter();\n        const obs3 = fromSharedEvent(emitter2, 'foo');\n        const obs4 = fromSharedEvent(emitter2, 'bar');\n        expect(obs1).not.toBe(obs3);\n        expect(obs2).not.toBe(obs4);\n    });\n\n    it('sets up listener only once per event emitter/name pair', () => {\n        const emitter = new EventEmitter();\n        const observable = fromSharedEvent(emitter, 'foo');\n        observable.subscribe();\n        observable.subscribe();\n\n        expect(emitter.listenerCount('foo')).toBe(1);\n    });\n});\n"
  },
  {
    "path": "lib/observables.ts",
    "content": "import EventEmitter from 'node:events';\n\nimport { fromEvent, Observable, share } from 'rxjs';\n\nconst sharedEvents = new WeakMap<EventEmitter, Map<string, Observable<unknown>>>();\n\n/**\n * Creates an observable for a specific event of an `EventEmitter` instance.\n *\n * The underlying event listener is set up only once across the application for that event emitter/name pair.\n */\nexport function fromSharedEvent(emitter: EventEmitter, event: string): Observable<unknown> {\n    let emitterEvents = sharedEvents.get(emitter);\n    if (!emitterEvents) {\n        emitterEvents = new Map();\n        sharedEvents.set(emitter, emitterEvents);\n    }\n\n    let observable = emitterEvents.get(event);\n    if (!observable) {\n        observable = fromEvent(emitter, event).pipe(share());\n        emitterEvents.set(event, observable);\n    }\n\n    return observable;\n}\n"
  },
  {
    "path": "lib/output-writer.spec.ts",
    "content": "import { Writable } from 'node:stream';\n\nimport { beforeEach, describe, expect, it, MockedObject } from 'vitest';\n\nimport { createMockInstance } from './__fixtures__/create-mock-instance.js';\nimport { createFakeCloseEvent, FakeCommand } from './__fixtures__/fake-command.js';\nimport { OutputWriter } from './output-writer.js';\n\nlet outputStream: MockedObject<Writable>;\nlet commands: FakeCommand[];\n\nfunction createWriter(overrides?: { group: boolean }) {\n    const options = {\n        outputStream,\n        group: false,\n        commands,\n        ...overrides,\n    };\n    return new OutputWriter(options);\n}\n\nfunction closeCommand(command: FakeCommand) {\n    command.state = 'exited';\n    command.close.next(createFakeCloseEvent({ command, index: command.index }));\n}\n\nbeforeEach(() => {\n    outputStream = createMockInstance(Writable);\n    commands = [\n        new FakeCommand('', undefined, 0),\n        new FakeCommand('', undefined, 1),\n        new FakeCommand('', undefined, 2),\n    ];\n});\n\nit('throws if outputStream already is in errored state', () => {\n    Object.defineProperty(outputStream, 'errored', { value: new Error('test') });\n    expect(() => createWriter()).toThrow(TypeError);\n});\n\ndescribe('#write()', () => {\n    it('throws if outputStream has errored', () => {\n        const writer = createWriter();\n        Object.defineProperty(outputStream, 'errored', { value: new Error('test') });\n        expect(() => writer.write(commands[0], 'hello')).toThrow(TypeError);\n    });\n\n    describe('with group=false', () => {\n        it('writes instantly', () => {\n            const writer = createWriter({ group: false });\n            writer.write(commands[2], 'hello');\n            expect(outputStream.write).toHaveBeenCalledExactlyOnceWith('hello');\n        });\n    });\n\n    describe('with group=true', () => {\n        it('writes for null commands', () => {\n            const writer = createWriter({ group: true });\n            writer.write(undefined, 'hello');\n            expect(outputStream.write).toHaveBeenCalledExactlyOnceWith('hello');\n        });\n\n        it('does not write instantly for non-active command', () => {\n            const writer = createWriter({ group: true });\n            writer.write(commands[2], 'hello');\n            expect(outputStream.write).toHaveBeenCalledTimes(0);\n            expect(writer.buffers[2]).toEqual(['hello']);\n        });\n\n        it('write instantly for active command', () => {\n            const writer = createWriter({ group: true });\n            writer.write(commands[0], 'hello');\n            expect(outputStream.write).toHaveBeenCalledExactlyOnceWith('hello');\n        });\n\n        it('does not wait for write from next command to flush', () => {\n            const writer = createWriter({ group: true });\n            writer.write(commands[1], 'hello');\n            writer.write(commands[1], 'foo bar');\n            expect(outputStream.write).toHaveBeenCalledTimes(0);\n            closeCommand(commands[0]);\n            expect(outputStream.write).toHaveBeenCalledTimes(2);\n            expect(writer.activeCommandIndex).toBe(1);\n            outputStream.write.mockClear();\n\n            writer.write(commands[1], 'blah');\n            expect(outputStream.write).toHaveBeenCalledTimes(1);\n        });\n\n        it('does not flush for non-active command', () => {\n            const writer = createWriter({ group: true });\n            writer.write(commands[1], 'hello');\n            writer.write(commands[1], 'foo bar');\n            expect(outputStream.write).toHaveBeenCalledTimes(0);\n            closeCommand(commands[1]);\n            expect(outputStream.write).toHaveBeenCalledTimes(0);\n            closeCommand(commands[0]);\n            expect(outputStream.write).toHaveBeenCalledTimes(2);\n        });\n\n        it('flushes multiple commands at a time if necessary', () => {\n            const writer = createWriter({ group: true });\n            writer.write(commands[2], 'hello');\n            closeCommand(commands[1]);\n            closeCommand(commands[2]);\n            expect(outputStream.write).toHaveBeenCalledTimes(0);\n            closeCommand(commands[0]);\n            expect(outputStream.write).toHaveBeenCalledExactlyOnceWith('hello');\n            expect(writer.activeCommandIndex).toBe(2);\n        });\n    });\n});\n"
  },
  {
    "path": "lib/output-writer.ts",
    "content": "import { Writable } from 'node:stream';\n\nimport Rx from 'rxjs';\n\nimport { Command } from './command.js';\nimport { fromSharedEvent } from './observables.js';\n\n/**\n * Class responsible for actually writing output onto a writable stream.\n */\nexport class OutputWriter {\n    private readonly outputStream: Writable;\n    private readonly group: boolean;\n    readonly buffers: string[][];\n    activeCommandIndex = 0;\n\n    readonly error: Rx.Observable<unknown>;\n    private get errored() {\n        return this.outputStream.errored;\n    }\n\n    constructor({\n        outputStream,\n        group,\n        commands,\n    }: {\n        outputStream: Writable;\n        group: boolean;\n        commands: Command[];\n    }) {\n        this.outputStream = outputStream;\n        this.ensureWritable();\n\n        this.error = fromSharedEvent(this.outputStream, 'error');\n        this.group = group;\n        this.buffers = commands.map(() => []);\n\n        if (this.group) {\n            Rx.merge(...commands.map((c) => c.close)).subscribe((command) => {\n                if (command.index !== this.activeCommandIndex) {\n                    return;\n                }\n                for (let i = command.index + 1; i < commands.length; i++) {\n                    this.activeCommandIndex = i;\n                    this.flushBuffer(i);\n                    // TODO: Should errored commands also flush buffer?\n                    if (commands[i].state !== 'exited') {\n                        break;\n                    }\n                }\n            });\n        }\n    }\n\n    private ensureWritable() {\n        if (this.errored) {\n            throw new TypeError('outputStream is in errored state', { cause: this.errored });\n        }\n    }\n\n    write(command: Command | undefined, text: string) {\n        this.ensureWritable();\n        if (this.group && command) {\n            if (command.index <= this.activeCommandIndex) {\n                this.outputStream.write(text);\n            } else {\n                this.buffers[command.index].push(text);\n            }\n        } else {\n            // \"global\" logs (command=null) are output out of order\n            this.outputStream.write(text);\n        }\n    }\n\n    private flushBuffer(index: number) {\n        if (!this.errored) {\n            this.buffers[index].forEach((t) => this.outputStream.write(t));\n        }\n        this.buffers[index] = [];\n    }\n}\n"
  },
  {
    "path": "lib/prefix-color-selector.spec.ts",
    "content": "import { ChalkInstance } from 'chalk';\nimport { afterEach, describe, expect, it, vi } from 'vitest';\n\nimport { PrefixColorSelector } from './prefix-color-selector.js';\n\nafterEach(() => {\n    vi.restoreAllMocks();\n});\n\ndescribe('#getNextColor()', () => {\n    const customTests: Record<\n        string,\n        {\n            acceptableConsoleColors?: Array<keyof ChalkInstance>;\n            customColors?: string | string[];\n            expectedColors: string[];\n        }\n    > = {\n        'does not produce a color if prefixColors empty': {\n            customColors: [],\n            expectedColors: ['', '', ''],\n        },\n        'does not produce a color if prefixColors undefined': {\n            expectedColors: ['', '', ''],\n        },\n        'uses user defined prefix colors only, if no auto is used': {\n            customColors: ['red', 'green', 'blue'],\n            expectedColors: [\n                'red',\n                'green',\n                'blue',\n\n                // Uses last color if last color is not \"auto\"\n                'blue',\n                'blue',\n                'blue',\n            ],\n        },\n        'trims colors': {\n            customColors: ['  red  ', '  green  ', '  blue  '],\n            expectedColors: ['red', 'green', 'blue'],\n        },\n        'accepts a string value for customColors': {\n            customColors: 'red',\n            expectedColors: ['red', 'red'],\n        },\n        'picks varying colors when user defines an auto color': {\n            acceptableConsoleColors: ['green', 'blue'],\n            customColors: [\n                'red',\n                'green',\n                'auto',\n                'green',\n                'auto',\n                'green',\n                'auto',\n                'blue',\n                'auto',\n                'orange',\n            ],\n            expectedColors: [\n                // Custom colors\n                'red',\n                'green',\n                'blue', // Picks auto color \"blue\", not repeating consecutive \"green\" color\n                'green', // Manual\n                'blue', // Auto picks \"blue\" not to repeat last\n                'green', // Manual\n                'blue', // Auto picks \"blue\" again not to repeat last\n                'blue', // Manual\n                'green', // Auto picks \"green\" again not to repeat last\n                'orange',\n\n                // Uses last color if last color is not \"auto\"\n                'orange',\n                'orange',\n                'orange',\n            ],\n        },\n        'uses user defined colors then recurring auto colors without repeating consecutive colors':\n            {\n                acceptableConsoleColors: ['green', 'blue'],\n                customColors: ['red', 'green', 'auto'],\n                expectedColors: [\n                    // Custom colors\n                    'red',\n                    'green',\n\n                    // Picks auto colors, not repeating consecutive \"green\" color\n                    'blue',\n                    'green',\n                    'blue',\n                    'green',\n                ],\n            },\n        'can sometimes produce consecutive colors': {\n            acceptableConsoleColors: ['green', 'blue'],\n            customColors: ['blue', 'auto'],\n            expectedColors: [\n                // Custom colors\n                'blue',\n\n                // Picks auto colors\n                'green',\n                // Does not repeat custom colors for initial auto colors, i.e. does not use \"blue\" again so soon\n                'green', // Consecutive color picked, however practically there would be a lot of colors that need to be set in a particular order for this to occur\n                'blue',\n                'green',\n                'blue',\n                'green',\n                'blue',\n            ],\n        },\n        'considers the Bright variants of colors equal to the normal colors to avoid similar colors':\n            {\n                acceptableConsoleColors: ['greenBright', 'blueBright', 'green', 'blue', 'magenta'],\n                customColors: ['green', 'blue', 'auto'],\n                expectedColors: [\n                    // Custom colors\n                    'green',\n                    'blue',\n\n                    // Picks auto colors, not repeating green and blue colors and variants initially\n                    'magenta',\n\n                    // Picks auto colors\n                    'greenBright',\n                    'blueBright',\n                    'green',\n                    'blue',\n                    'magenta',\n                ],\n            },\n    };\n    it.each(Object.entries(customTests))(\n        '%s',\n        (_, { acceptableConsoleColors, customColors, expectedColors }) => {\n            if (acceptableConsoleColors) {\n                vi.spyOn(PrefixColorSelector, 'ACCEPTABLE_CONSOLE_COLORS', 'get').mockReturnValue(\n                    acceptableConsoleColors,\n                );\n            }\n            const prefixColorSelector = new PrefixColorSelector(customColors);\n            const prefixColorSelectorValues = expectedColors.map(() =>\n                prefixColorSelector.getNextColor(),\n            );\n\n            expect(prefixColorSelectorValues).toEqual(expectedColors);\n        },\n    );\n\n    const autoTests = {\n        'does not repeat consecutive colors when last prefixColor is auto': false,\n        'handles when more individual auto prefixColors exist than acceptable console colors': true,\n    };\n    it.each(Object.entries(autoTests))('%s', (_, map) => {\n        // Pick auto colors over 2 sets\n        const expectedColors: string[] = [\n            ...PrefixColorSelector.ACCEPTABLE_CONSOLE_COLORS,\n            ...PrefixColorSelector.ACCEPTABLE_CONSOLE_COLORS,\n        ];\n\n        const prefixColorSelector = new PrefixColorSelector(\n            map ? expectedColors.map(() => 'auto') : ['auto'],\n        );\n\n        let previousColor = '';\n        for (const expectedColor of expectedColors) {\n            const actualSelectedColor = prefixColorSelector.getNextColor();\n            expect(actualSelectedColor).not.toBe(previousColor); // No consecutive colors\n            expect(actualSelectedColor).toBe(expectedColor); // Expected color\n            previousColor = actualSelectedColor;\n        }\n    });\n});\n\ndescribe('#ACCEPTABLE_CONSOLE_COLORS', () => {\n    it('has more than 1 auto color defined', () => {\n        // (!) The current implementation is based on the assumption that 'ACCEPTABLE_CONSOLE_COLORS'\n        //     always has more than one entry, which is what we enforce via this test\n        expect(PrefixColorSelector.ACCEPTABLE_CONSOLE_COLORS.length).toBeGreaterThan(1);\n    });\n});\n"
  },
  {
    "path": "lib/prefix-color-selector.ts",
    "content": "import { ChalkInstance } from 'chalk';\n\nfunction getConsoleColorsWithoutCustomColors(customColors: string[]): string[] {\n    return PrefixColorSelector.ACCEPTABLE_CONSOLE_COLORS.filter(\n        // Consider the \"Bright\" variants of colors to be the same as the plain color to avoid similar colors\n        (color) => !customColors.includes(color.replace(/Bright$/, '')),\n    );\n}\n\n/**\n * Creates a generator that yields an infinite stream of colors.\n */\nfunction* createColorGenerator(customColors: string[]): Generator<string, string> {\n    // Custom colors should be used as is, except for \"auto\"\n    const nextAutoColors: string[] = getConsoleColorsWithoutCustomColors(customColors);\n    let lastColor: string | undefined;\n    for (const customColor of customColors) {\n        let currentColor = customColor;\n        if (currentColor !== 'auto') {\n            yield currentColor; // Manual color\n        } else {\n            // Find the first auto color that is not the same as the last color\n            while (currentColor === 'auto' || lastColor === currentColor) {\n                if (!nextAutoColors.length) {\n                    // There could be more \"auto\" values than auto colors so this needs to be able to refill\n                    nextAutoColors.push(...PrefixColorSelector.ACCEPTABLE_CONSOLE_COLORS);\n                }\n                currentColor = String(nextAutoColors.shift());\n            }\n            yield currentColor; // Auto color\n        }\n        lastColor = currentColor;\n    }\n\n    const lastCustomColor = customColors[customColors.length - 1] || '';\n    if (lastCustomColor !== 'auto') {\n        while (true) {\n            yield lastCustomColor; // If last custom color was not \"auto\" then return same color forever, to maintain existing behavior\n        }\n    }\n\n    // Finish the initial set(s) of auto colors to avoid repetition\n    for (const color of nextAutoColors) {\n        yield color;\n    }\n\n    // Yield an infinite stream of acceptable console colors\n    //\n    // If the given custom colors use every ACCEPTABLE_CONSOLE_COLORS except one then there is a chance a color will be repeated,\n    // however its highly unlikely and low consequence so not worth the extra complexity to account for it\n    while (true) {\n        for (const color of PrefixColorSelector.ACCEPTABLE_CONSOLE_COLORS) {\n            yield color; // Repeat colors forever\n        }\n    }\n}\n\nexport class PrefixColorSelector {\n    private colorGenerator: Generator<string, string>;\n\n    constructor(customColors: string | string[] = []) {\n        const normalizedColors = typeof customColors === 'string' ? [customColors] : customColors;\n        this.colorGenerator = createColorGenerator(normalizedColors);\n    }\n\n    /** A list of colors that are readable in a terminal. */\n    public static get ACCEPTABLE_CONSOLE_COLORS() {\n        // Colors picked randomly, can be amended if required\n        return [\n            // Prevent duplicates, in case the list becomes significantly large\n            ...new Set<keyof ChalkInstance>([\n                // Text colors\n                'cyan',\n                'yellow',\n                'greenBright',\n                'blueBright',\n                'magentaBright',\n                'white',\n                'grey',\n                'red',\n\n                // Background colors\n                'bgCyan',\n                'bgYellow',\n                'bgGreenBright',\n                'bgBlueBright',\n                'bgMagenta',\n                'bgWhiteBright',\n                'bgGrey',\n                'bgRed',\n            ]),\n        ];\n    }\n\n    /**\n     * @returns The given custom colors then a set of acceptable console colors indefinitely.\n     */\n    getNextColor(): string {\n        return this.colorGenerator.next().value.trim();\n    }\n}\n"
  },
  {
    "path": "lib/spawn.spec.ts",
    "content": "import { describe, expect, it, vi } from 'vitest';\n\nimport { getSpawnOpts, spawn } from './spawn.js';\n\nconst baseProcess = {\n    platform: 'win32' as const,\n    cwd: () => '',\n    env: {},\n};\n\ndescribe('spawn()', () => {\n    it('spawns the given command', async () => {\n        const fakeSpawn = vi.fn();\n        spawn('echo banana', {}, fakeSpawn, baseProcess);\n        expect(fakeSpawn).toHaveBeenCalled();\n        expect(fakeSpawn.mock.calls[0][1].join(' ')).toContain('echo banana');\n    });\n\n    it('returns spawned process', async () => {\n        const childProcess = {};\n        const fakeSpawn = vi.fn().mockReturnValue(childProcess);\n        const child = spawn('echo banana', {}, fakeSpawn, baseProcess);\n        expect(child).toBe(childProcess);\n    });\n});\n\ndescribe('getSpawnOpts()', () => {\n    it('sets detached mode to false for Windows platform', () => {\n        expect(getSpawnOpts({ process: baseProcess }).detached).toBe(false);\n    });\n\n    it('sets stdio to pipe when stdio mode is normal', () => {\n        expect(getSpawnOpts({ stdio: 'normal' }).stdio).toEqual(['pipe', 'pipe', 'pipe']);\n    });\n\n    it('sets stdio to inherit when stdio mode is raw', () => {\n        expect(getSpawnOpts({ stdio: 'raw' }).stdio).toEqual(['inherit', 'inherit', 'inherit']);\n    });\n\n    it('sets stdio to ignore stdout + stderr when stdio mode is hidden', () => {\n        expect(getSpawnOpts({ stdio: 'hidden' }).stdio).toEqual(['pipe', 'ignore', 'ignore']);\n    });\n\n    it('sets an ipc channel at the specified descriptor index', () => {\n        const opts = getSpawnOpts({ ipc: 3 });\n        expect(opts.stdio?.[3]).toBe('ipc');\n    });\n\n    it('throws if the ipc channel is <= 2', () => {\n        const fn = () => getSpawnOpts({ ipc: 0 });\n        expect(fn).toThrow();\n    });\n\n    it('merges FORCE_COLOR into env vars if color supported', () => {\n        const process = { ...baseProcess, env: { foo: 'bar' } };\n        expect(getSpawnOpts({ process, colorSupport: false }).env).toEqual(process.env);\n        expect(getSpawnOpts({ process, colorSupport: { level: 1 } }).env).toEqual({\n            FORCE_COLOR: '1',\n            foo: 'bar',\n        });\n    });\n\n    it('sets default cwd to process.cwd()', () => {\n        const process = { ...baseProcess, cwd: () => 'process-cwd' };\n        expect(getSpawnOpts({ process }).cwd).toBe('process-cwd');\n    });\n\n    it('overrides default cwd', () => {\n        const cwd = 'foobar';\n        expect(getSpawnOpts({ cwd }).cwd).toBe(cwd);\n    });\n});\n"
  },
  {
    "path": "lib/spawn.ts",
    "content": "import assert from 'node:assert';\nimport { ChildProcess, IOType, spawn as baseSpawn, SpawnOptions } from 'node:child_process';\nimport nodeProcess from 'node:process';\n\nimport supportsColor, { ColorSupport } from 'supports-color';\n\n/**\n * Spawns a command using `cmd.exe` on Windows, or `/bin/sh` elsewhere.\n */\n// Implementation based off of https://github.com/mmalecki/spawn-command/blob/v0.0.2-1/lib/spawn-command.js\nexport function spawn(\n    command: string,\n    options: SpawnOptions,\n    // For testing\n    spawn: (command: string, args: string[], options: SpawnOptions) => ChildProcess = baseSpawn,\n    process: Pick<NodeJS.Process, 'platform'> = nodeProcess,\n): ChildProcess {\n    let file = '/bin/sh';\n    let args = ['-c', command];\n    if (process.platform === 'win32') {\n        file = 'cmd.exe';\n        args = ['/s', '/c', `\"${command}\"`];\n        options.windowsVerbatimArguments = true;\n    }\n    return spawn(file, args, options);\n}\n\nexport const getSpawnOpts = ({\n    colorSupport = supportsColor.stdout,\n    cwd,\n    process = nodeProcess,\n    ipc,\n    stdio = 'normal',\n    env = {},\n}: {\n    /**\n     * What the color support of the spawned processes should be.\n     * If set to `false`, then no colors should be output.\n     *\n     * Defaults to whatever the terminal's stdout support is.\n     */\n    colorSupport?: Pick<ColorSupport, 'level'> | false;\n\n    /**\n     * The NodeJS process.\n     */\n    process?: Pick<NodeJS.Process, 'cwd' | 'platform' | 'env'>;\n\n    /**\n     * A custom working directory to spawn processes in.\n     * Defaults to `process.cwd()`.\n     */\n    cwd?: string;\n\n    /**\n     * The file descriptor number at which the channel for inter-process communication\n     * should be set up.\n     */\n    ipc?: number;\n\n    /**\n     * Which stdio mode to use. Raw implies inheriting the parent process' stdio.\n     *\n     * - `normal`: all of stdout, stderr and stdin are piped\n     * - `hidden`: stdin is piped, stdout/stderr outputs are ignored\n     * - `raw`: all of stdout, stderr and stdin are inherited from the main process\n     *\n     * Defaults to `normal`.\n     */\n    stdio?: 'normal' | 'hidden' | 'raw';\n\n    /**\n     * Map of custom environment variables to include in the spawn options.\n     */\n    env?: Record<string, unknown>;\n}): SpawnOptions => {\n    const stdioValues: (IOType | 'ipc')[] =\n        stdio === 'normal'\n            ? ['pipe', 'pipe', 'pipe']\n            : stdio === 'raw'\n              ? ['inherit', 'inherit', 'inherit']\n              : ['pipe', 'ignore', 'ignore'];\n\n    if (ipc != null) {\n        // Avoid overriding the stdout/stderr/stdin\n        assert.ok(ipc > 2, '[concurrently] the IPC channel number should be > 2');\n        stdioValues[ipc] = 'ipc';\n    }\n\n    return {\n        cwd: cwd || process.cwd(),\n        stdio: stdioValues,\n        ...(process.platform.startsWith('win') && { detached: false }),\n        env: {\n            ...(colorSupport ? { FORCE_COLOR: colorSupport.level.toString() } : {}),\n            ...process.env,\n            ...env,\n        },\n    };\n};\n"
  },
  {
    "path": "lib/utils.spec.ts",
    "content": "import { describe, expect, it } from 'vitest';\n\nimport { castArray, escapeRegExp } from './utils.js';\n\ndescribe('#escapeRegExp()', () => {\n    it('escapes all RegExp chars', () => {\n        // eslint-disable-next-line no-useless-escape\n        const result = escapeRegExp('\\*?{}.(?<test>.)|[]');\n\n        expect(result).toBe('\\\\*\\\\?\\\\{\\\\}\\\\.\\\\(\\\\?<test>\\\\.\\\\)\\\\|\\\\[\\\\]');\n    });\n});\n\ndescribe('#castArray()', () => {\n    it('returns empty array for nullish input values', () => {\n        const result1 = castArray();\n        const result2 = castArray(undefined);\n        const result3 = castArray(null);\n\n        expect(result1).toStrictEqual([]);\n        expect(result2).toStrictEqual([]);\n        expect(result3).toStrictEqual([]);\n    });\n\n    it('directly returns value if it is already of type array', () => {\n        const value = ['example'];\n        const result = castArray(value);\n\n        expect(result).toBe(value);\n    });\n\n    describe('casts primitives to an array', () => {\n        it.each([1, 'example', {}])('%s', (value) => {\n            const result = castArray(value);\n\n            expect(result).toStrictEqual([value]);\n        });\n    });\n});\n"
  },
  {
    "path": "lib/utils.ts",
    "content": "/**\n * Escapes a string for use in a regular expression.\n */\nexport function escapeRegExp(str: string) {\n    return str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\ntype CastArrayResult<T> = T extends undefined | null ? never[] : T extends unknown[] ? T : T[];\n\n/**\n * Casts a value to an array if it's not one.\n */\nexport function castArray<T = never[]>(value?: T) {\n    return (Array.isArray(value) ? value : value != null ? [value] : []) as CastArrayResult<T>;\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"concurrently\",\n  \"type\": \"module\",\n  \"version\": \"9.2.1\",\n  \"packageManager\": \"pnpm@10.18.2+sha512.9fb969fa749b3ade6035e0f109f0b8a60b5d08a1a87fdf72e337da90dcc93336e2280ca4e44f2358a649b83c17959e9993e777c2080879f3801e6f0d999ad3dd\",\n  \"description\": \"Run commands concurrently\",\n  \"author\": \"Kimmo Brunfeldt\",\n  \"license\": \"MIT\",\n  \"funding\": \"https://github.com/open-cli-tools/concurrently?sponsor=1\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/open-cli-tools/concurrently.git\"\n  },\n  \"keywords\": [\n    \"bash\",\n    \"concurrent\",\n    \"parallel\",\n    \"concurrently\",\n    \"command\",\n    \"sh\"\n  ],\n  \"exports\": {\n    \".\": \"./dist/lib/index.js\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"types\": \"./dist/lib/index.d.ts\",\n  \"publishConfig\": {\n    \"bin\": {\n      \"concurrently\": \"./dist/bin/index.js\",\n      \"conc\": \"./dist/bin/index.js\"\n    }\n  },\n  \"files\": [\n    \"!**/*.spec.d.ts\",\n    \"!**/*.spec.js\",\n    \"!**/__fixtures__\",\n    \"dist\",\n    \"dist/tsconfig.tsbuildinfo\",\n    \"docs\"\n  ],\n  \"engines\": {\n    \"node\": \">=20\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc --build\",\n    \"postbuild\": \"chmod +x dist/bin/index.js\",\n    \"typecheck\": \"tsc --noEmit\",\n    \"format\": \"prettier --check '**/*.{json,y?(a)ml,md}'\",\n    \"lint\": \"eslint\",\n    \"prepublishOnly\": \"safe-publish-latest && pnpm run build\",\n    \"test\": \"vitest --project unit\",\n    \"test:smoke\": \"vitest run --project smoke\",\n    \"prepare\": \"husky\"\n  },\n  \"dependencies\": {\n    \"chalk\": \"5.6.2\",\n    \"rxjs\": \"7.8.2\",\n    \"shell-quote\": \"1.8.3\",\n    \"supports-color\": \"10.2.2\",\n    \"tree-kill\": \"1.2.2\",\n    \"yargs\": \"17.7.2\"\n  },\n  \"devDependencies\": {\n    \"@eslint/js\": \"^9.37.0\",\n    \"@hirez_io/observer-spy\": \"^2.2.0\",\n    \"@types/node\": \"^20.19.20\",\n    \"@types/shell-quote\": \"^1.7.5\",\n    \"@types/yargs\": \"^17.0.33\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"@vitest/eslint-plugin\": \"^1.3.16\",\n    \"ctrlc-wrapper\": \"^0.0.5\",\n    \"esbuild\": \"~0.25.10\",\n    \"eslint\": \"^9.37.0\",\n    \"eslint-config-flat-gitignore\": \"^2.1.0\",\n    \"eslint-config-prettier\": \"^10.1.8\",\n    \"eslint-plugin-import-lite\": \"^0.3.0\",\n    \"eslint-plugin-prettier\": \"^5.5.4\",\n    \"eslint-plugin-simple-import-sort\": \"^12.1.1\",\n    \"globals\": \"^16.4.0\",\n    \"husky\": \"^9.1.7\",\n    \"lint-staged\": \"^16.2.3\",\n    \"prettier\": \"^3.6.2\",\n    \"safe-publish-latest\": \"^2.0.0\",\n    \"string-argv\": \"^0.3.2\",\n    \"typescript\": \"~5.9.3\",\n    \"typescript-eslint\": \"^8.46.0\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"lint-staged\": {\n    \"*.{js,ts}\": \"eslint --fix\",\n    \"*.{json,y?(a)ml,md}\": \"prettier --write\"\n  }\n}\n"
  },
  {
    "path": "pnpm-workspace.yaml",
    "content": "packages:\n  - tests\n\ninjectWorkspacePackages: false\n\nonlyBuiltDependencies:\n  - esbuild\n"
  },
  {
    "path": "tests/cjs-import/package.json",
    "content": "{\n  \"type\": \"commonjs\"\n}\n"
  },
  {
    "path": "tests/cjs-import/smoke-test.ts",
    "content": "import type { ConcurrentlyResult } from 'concurrently';\nimport concurrently, { concurrently as concurrently2, createConcurrently } from 'concurrently';\n\nconst _result: ConcurrentlyResult = concurrently(['echo test'], {\n    raw: true,\n});\n\nconst _result2: ConcurrentlyResult = concurrently2(['echo test'], {\n    killOthersOn: ['failure'],\n});\n\nconst _result3: ConcurrentlyResult = createConcurrently(['echo test'], {\n    successCondition: 'all',\n});\n"
  },
  {
    "path": "tests/cjs-import/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"module\": \"commonjs\",\n    \"outDir\": \"dist\",\n    \"skipLibCheck\": true\n  }\n}\n"
  },
  {
    "path": "tests/cjs-require/package.json",
    "content": "{\n  \"type\": \"commonjs\"\n}\n"
  },
  {
    "path": "tests/cjs-require/smoke-test.ts",
    "content": "// eslint-disable-next-line @typescript-eslint/no-require-imports\nimport concurrently = require('concurrently');\n\nconst { concurrently: concurrently2, createConcurrently } = concurrently;\n\nconst _result: concurrently.ConcurrentlyResult = concurrently.default(['echo test'], {\n    raw: true,\n});\n\nconst _result2: concurrently.ConcurrentlyResult = concurrently2(['echo test'], {\n    killOthersOn: ['failure'],\n});\n\nconst _result3: concurrently.ConcurrentlyResult = createConcurrently(['echo test'], {\n    successCondition: 'all',\n});\n"
  },
  {
    "path": "tests/cjs-require/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"module\": \"commonjs\",\n    \"outDir\": \"dist\",\n    \"skipLibCheck\": true\n  }\n}\n"
  },
  {
    "path": "tests/esm/package.json",
    "content": "{\n  \"type\": \"module\"\n}\n"
  },
  {
    "path": "tests/esm/smoke-test.ts",
    "content": "import type { ConcurrentlyResult } from 'concurrently';\nimport concurrently, { concurrently as concurrently2, createConcurrently } from 'concurrently';\n\nconst _result: ConcurrentlyResult = concurrently(['echo test'], {\n    raw: true,\n});\n\nconst _result2: ConcurrentlyResult = concurrently2(['echo test'], {\n    killOthersOn: ['failure'],\n});\n\nconst _result3: ConcurrentlyResult = createConcurrently(['echo test'], {\n    successCondition: 'all',\n});\n"
  },
  {
    "path": "tests/esm/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"module\": \"node20\",\n    \"outDir\": \"dist\",\n    \"skipLibCheck\": true\n  }\n}\n"
  },
  {
    "path": "tests/package.json",
    "content": "{\n  \"dependencies\": {\n    \"concurrently\": \"workspace:*\"\n  },\n  \"scripts\": {\n    \"test\": \"pnpm --workspace-root test:smoke\"\n  }\n}\n"
  },
  {
    "path": "tests/smoke-tests.spec.ts",
    "content": "import { exec as originalExec } from 'node:child_process';\nimport util from 'node:util';\n\nimport { beforeAll, expect, it } from 'vitest';\n\nconst exec = util.promisify(originalExec);\n\nbeforeAll(async () => {\n    await exec('pnpm run build');\n}, 20_000);\n\nit('spawns binary', async () => {\n    await expect(exec('node dist/bin/index.js \"echo test\"')).resolves.toBeDefined();\n});\n\nit.each(['cjs-import', 'cjs-require', 'esm'])('loads library in %s context', async (project) => {\n    // Use as separate execs as tsc outputs to stdout, instead of stderr, and so its text isn't shown\n    await exec(`tsc -p ${project}`, { cwd: __dirname }).catch((err) => Promise.reject(err.stdout));\n    await expect(\n        exec(`node ${project}/dist/smoke-test.js`, { cwd: __dirname }),\n    ).resolves.toBeDefined();\n});\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"lib\": [\"es2023\"],\n    \"module\": \"node20\",\n\n    \"outDir\": \"dist\",\n    \"declaration\": true,\n\n    \"strict\": true,\n    \"skipLibCheck\": true\n  },\n  \"include\": [\"./bin\", \"./lib\"]\n}\n"
  },
  {
    "path": "vitest.config.ts",
    "content": "import { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n    test: {\n        coverage: {\n            include: ['lib/**/*.ts', '!lib/index.ts'],\n            // lcov is used for coveralls\n            reporter: ['text', 'html', 'lcov'],\n        },\n        projects: [\n            {\n                extends: true,\n                test: {\n                    name: 'unit',\n                    include: ['{bin,lib}/**/*.spec.ts'],\n                },\n            },\n            {\n                extends: true,\n                test: {\n                    name: 'smoke',\n                    include: ['tests/**/*.spec.ts'],\n                },\n            },\n        ],\n    },\n});\n"
  }
]