Fix bin/publish: copy docs.dist from project root

Fix bin/publish: use correct .env path for rspade_system
Fix bin/publish script: prevent grep exit code 1 from terminating script

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-10-21 02:08:33 +00:00
commit f6fac6c4bc
79758 changed files with 10547827 additions and 0 deletions

21
vendor/spatie/ignition/node_modules/concurrently/LICENSE generated vendored Executable file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Kimmo Brunfeldt
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

311
vendor/spatie/ignition/node_modules/concurrently/README.md generated vendored Executable file
View File

@@ -0,0 +1,311 @@
# Concurrently
[![Build Status](https://github.com/open-cli-tools/concurrently/workflows/Tests/badge.svg)](https://github.com/open-cli-tools/concurrently/actions?workflow=Tests)
[![Coverage Status](https://coveralls.io/repos/github/open-cli-tools/concurrently/badge.svg?branch=master)](https://coveralls.io/github/open-cli-tools/concurrently?branch=master)
[![NPM Badge](https://nodei.co/npm/concurrently.png?downloads=true)](https://www.npmjs.com/package/concurrently)
Run multiple commands concurrently.
Like `npm run watch-js & npm run watch-less` but better.
![](docs/demo.gif)
**Table of contents**
- [Concurrently](#concurrently)
- [Why](#why)
- [Install](#install)
- [Usage](#usage)
- [Programmatic Usage](#programmatic-usage)
- [`concurrently(commands[, options])`](#concurrentlycommands-options)
- [FAQ](#faq)
## Why
I like [task automation with npm](https://github.com/substack/blog/blob/master/npm_run.markdown)
but the usual way to run multiple commands concurrently is
`npm run watch-js & npm run watch-css`. That's fine but it's hard to keep
on track of different outputs. Also if one process fails, others still keep running
and you won't even notice the difference.
Another option would be to just run all commands in separate terminals. I got
tired of opening terminals and made **concurrently**.
**Features:**
* Cross platform (including Windows)
* Output is easy to follow with prefixes
* With `--kill-others` switch, all commands are killed if one dies
* Spawns commands with [spawn-command](https://github.com/mmalecki/spawn-command)
## Install
The tool is written in Node.js, but you can use it to run **any** commands.
```bash
npm install -g concurrently
```
or if you are using it from npm scripts:
```bash
npm install concurrently --save
```
## Usage
Remember to surround separate commands with quotes:
```bash
concurrently "command1 arg" "command2 arg"
```
Otherwise **concurrently** would try to run 4 separate commands:
`command1`, `arg`, `command2`, `arg`.
In package.json, escape quotes:
```bash
"start": "concurrently \"command1 arg\" \"command2 arg\""
```
NPM run commands can be shortened:
```bash
concurrently "npm:watch-js" "npm:watch-css" "npm:watch-node"
# Equivalent to:
concurrently -n watch-js,watch-css,watch-node "npm run watch-js" "npm run watch-css" "npm run watch-node"
```
NPM shortened commands also support wildcards. Given the following scripts in
package.json:
```javascript
{
//...
"scripts": {
// ...
"watch-js": "...",
"watch-css": "...",
"watch-node": "...",
// ...
},
// ...
}
```
```bash
concurrently "npm:watch-*"
# Equivalent to:
concurrently -n js,css,node "npm run watch-js" "npm run watch-css" "npm run watch-node"
# Any name provided for the wildcard command will be used as a prefix to the wildcard
# part of the script name:
concurrently -n w: npm:watch-*
# Equivalent to:
concurrently -n w:js,w:css,w:node "npm run watch-js" "npm run watch-css" "npm run watch-node"
```
Good frontend one-liner example [here](https://github.com/kimmobrunfeldt/dont-copy-paste-this-frontend-template/blob/5cd2bde719654941bdfc0a42c6f1b8e69ae79980/package.json#L9).
Help:
```
concurrently [options] <command ...>
General
-m, --max-processes How many processes should run at once.
New processes only spawn after all restart tries of a
process. [number]
-n, --names List of custom names to be used in prefix template.
Example names: "main,browser,server" [string]
--name-separator The character to split <names> on. Example usage:
concurrently -n "styles|scripts|server" --name-separator
"|" [default: ","]
-r, --raw Output only raw output of processes, disables
prettifying and concurrently coloring. [boolean]
-s, --success Return exit code of zero or one based on the success or
failure of the "first" child to terminate, the "last
child", or succeed only if "all" child processes
succeed.
[choices: "first", "last", "all"] [default: "all"]
--no-color Disables colors from logging [boolean]
--hide Comma-separated list of processes to hide the output.
The processes can be identified by their name or index.
[string] [default: ""]
Prefix styling
-p, --prefix Prefix used in logging for each process.
Possible values: index, pid, time, command, name,
none, or a template. Example template: "{time}-{pid}"
[string] [default: index or name (when --names is set)]
-c, --prefix-colors Comma-separated list of chalk colors to use on
prefixes. If there are more commands than colors, the
last color will be repeated.
- Available modifiers: reset, bold, dim, italic,
underline, inverse, hidden, strikethrough
- Available colors: black, red, green, yellow, blue,
magenta, cyan, white, gray, or any hex values for
colors, eg #23de43
- Available background colors: bgBlack, bgRed,
bgGreen, bgYellow, bgBlue, bgMagenta, bgCyan, bgWhite
See https://www.npmjs.com/package/chalk for more
information. [string] [default: "reset"]
-l, --prefix-length Limit how many characters of the command is displayed
in prefix. The option can be used to shorten the
prefix when it is set to "command"
[number] [default: 10]
-t, --timestamp-format Specify the timestamp in moment/date-fns format.
[string] [default: "yyyy-MM-dd HH:mm:ss.SSS"]
Input handling
-i, --handle-input Whether input should be forwarded to the child
processes. See examples for more information.[boolean]
--default-input-target Identifier for child process to which input on stdin
should be sent if not specified at start of input.
Can be either the index or the name of the process.
[default: 0]
Killing other processes
-k, --kill-others kill other processes if one exits or dies [boolean]
--kill-others-on-fail kill other processes if one exits with non zero status
code [boolean]
Restarting
--restart-tries How many times a process that died should restart.
Negative numbers will make the process restart forever.
[number] [default: 0]
--restart-after Delay time to respawn the process, in milliseconds.
[number] [default: 0]
Options:
-h, --help Show help [boolean]
-v, -V, --version Show version number [boolean]
Examples:
- Output nothing more than stdout+stderr of child processes
$ concurrently --raw "npm run watch-less" "npm run watch-js"
- Normal output but without colors e.g. when logging to file
$ concurrently --no-color "grunt watch" "http-server" > log
- Custom prefix
$ concurrently --prefix "{time}-{pid}" "npm run watch" "http-server"
- Custom names and colored prefixes
$ concurrently --names "HTTP,WATCH" -c "bgBlue.bold,bgMagenta.bold"
"http-server" "npm run watch"
- Send input to default
$ concurrently --handle-input "nodemon" "npm run watch-js"
rs # Sends rs command to nodemon process
- Send input to specific child identified by index
$ concurrently --handle-input "npm run watch-js" nodemon
1:rs
- Send input to specific child identified by name
$ concurrently --handle-input -n js,srv "npm run watch-js" nodemon
srv:rs
- Shortened NPM run commands
$ concurrently npm:watch-node npm:watch-js npm:watch-css
- Shortened NPM run command with wildcard
$ concurrently npm:watch-*
For more details, visit https://github.com/open-cli-tools/concurrently
```
## Programmatic Usage
concurrently can be used programmatically by using the API documented below:
### `concurrently(commands[, options])`
- `commands`: an array of either strings (containing the commands to run) or objects
with the shape `{ command, name, prefixColor, env, cwd }`.
- `options` (optional): an object containing any of the below:
- `cwd`: the working directory to be used by all commands. Can be overriden per command.
Default: `process.cwd()`.
- `defaultInputTarget`: the default input target when reading from `inputStream`.
Default: `0`.
- `handleInput`: when `true`, reads input from `process.stdin`.
- `inputStream`: a [`Readable` stream](https://nodejs.org/dist/latest-v10.x/docs/api/stream.html#stream_readable_streams)
to read the input from. Should only be used in the rare instance you would like to stream anything other than `process.stdin`. Overrides `handleInput`.
- `pauseInputStreamOnFinish`: by default, pauses the input stream (`process.stdin` when `handleInput` is enabled, or `inputStream` if provided) when all of the processes have finished. If you need to read from the input stream after `concurrently` has finished, set this to `false`. ([#252](https://github.com/kimmobrunfeldt/concurrently/issues/252)).
- `killOthers`: an array of exitting conditions that will cause a process to kill others.
Can contain any of `success` or `failure`.
- `maxProcesses`: how many processes should run at once.
- `outputStream`: a [`Writable` stream](https://nodejs.org/dist/latest-v10.x/docs/api/stream.html#stream_writable_streams)
to write logs to. Default: `process.stdout`.
- `prefix`: the prefix type to use when logging processes output.
Possible values: `index`, `pid`, `time`, `command`, `name`, `none`, or a template (eg `[{time} process: {pid}]`).
Default: the name of the process, or its index if no name is set.
- `prefixColors`: a list of colors as supported by [chalk](https://www.npmjs.com/package/chalk).
If concurrently would run more commands than there are colors, the last color is repeated.
Prefix colors specified per-command take precedence over this list.
- `prefixLength`: how many characters to show when prefixing with `command`. Default: `10`
- `raw`: whether raw mode should be used, meaning strictly process output will
be logged, without any prefixes, colouring or extra stuff.
- `successCondition`: the condition to consider the run was successful.
If `first`, only the first process to exit will make up the success of the run; if `last`, the last process that exits will determine whether the run succeeds.
Anything else means all processes should exit successfully.
- `restartTries`: how many attempts to restart a process that dies will be made. Default: `0`.
- `restartDelay`: how many milliseconds to wait between process restarts. Default: `0`.
- `timestampFormat`: a [date-fns format](https://date-fns.org/v2.0.1/docs/format)
to use when prefixing with `time`. Default: `yyyy-MM-dd HH:mm:ss.ZZZ`
> Returns: a `Promise` that resolves if the run was successful (according to `successCondition` option),
> or rejects, containing an array of objects with information for each command that has been run, in the order
> that the commands terminated. The objects have the shape `{ command, index, exitCode, killed }`, where `command` is the object
> passed in the `commands` array, `index` its index there and `killed` indicates if the process was killed as a result of
> `killOthers`. Default values (empty strings or objects) are returned for the fields that were not specified.
Example:
```js
const concurrently = require('concurrently');
concurrently([
'npm:watch-*',
{ command: 'nodemon', name: 'server' },
{ command: 'deploy', name: 'deploy', env: { PUBLIC_KEY: '...' } },
{ command: 'watch', name: 'watch', cwd: path.resolve(__dirname, 'scripts/watchers')}
], {
prefix: 'name',
killOthers: ['failure', 'success'],
restartTries: 3,
cwd: path.resolve(__dirname, 'scripts'),
}).then(success, failure);
```
## FAQ
* Process exited with code *null*?
From [Node child_process documentation](http://nodejs.org/api/child_process.html#child_process_event_exit), `exit` event:
> This event is emitted after the child process ends. If the process
> terminated normally, code is the final exit code of the process,
> otherwise null. If the process terminated due to receipt of a signal,
> signal is the string name of the signal, otherwise null.
So *null* means the process didn't terminate normally. This will make **concurrent**
to return non-zero exit code too.
* Does this work with the npm-replacements [yarn](https://github.com/yarnpkg/yarn) or [pnpm](https://pnpm.js.org/)?
Yes! In all examples above, you may replace "`npm`" with "`yarn`" or "`pnpm`".

View File

@@ -0,0 +1,178 @@
#!/usr/bin/env node
const fs = require('fs');
const yargs = require('yargs');
const defaults = require('../src/defaults');
const concurrently = require('../index');
const args = yargs
.usage('$0 [options] <command ...>')
.help('h')
.alias('h', 'help')
.version('v', require('../package.json').version)
.alias('v', 'V')
.alias('v', 'version')
.options({
// General
'm': {
alias: 'max-processes',
describe:
'How many processes should run at once.\n' +
'New processes only spawn after all restart tries of a process.',
type: 'number'
},
'n': {
alias: 'names',
describe:
'List of custom names to be used in prefix template.\n' +
'Example names: "main,browser,server"',
type: 'string'
},
'name-separator': {
describe:
'The character to split <names> on. Example usage:\n' +
'concurrently -n "styles|scripts|server" --name-separator "|"',
default: defaults.nameSeparator,
},
's': {
alias: 'success',
describe:
'Return exit code of zero or one based on the success or failure ' +
'of the "first" child to terminate, the "last child", or succeed ' +
'only if "all" child processes succeed.',
choices: ['first', 'last', 'all'],
default: defaults.success
},
'r': {
alias: 'raw',
describe:
'Output only raw output of processes, disables prettifying ' +
'and concurrently coloring.',
type: 'boolean'
},
// This one is provided for free. Chalk reads this itself and removes colours.
// https://www.npmjs.com/package/chalk#chalksupportscolor
'no-color': {
describe: 'Disables colors from logging',
type: 'boolean'
},
'hide': {
describe:
'Comma-separated list of processes to hide the output.\n' +
'The processes can be identified by their name or index.',
default: defaults.hide,
type: 'string'
},
// Kill others
'k': {
alias: 'kill-others',
describe: 'kill other processes if one exits or dies',
type: 'boolean'
},
'kill-others-on-fail': {
describe: 'kill other processes if one exits with non zero status code',
type: 'boolean'
},
// Prefix
'p': {
alias: 'prefix',
describe:
'Prefix used in logging for each process.\n' +
'Possible values: index, pid, time, command, name, none, or a template. ' +
'Example template: "{time}-{pid}"',
defaultDescription: 'index or name (when --names is set)',
type: 'string'
},
'c': {
alias: 'prefix-colors',
describe:
'Comma-separated list of chalk colors to use on prefixes. ' +
'If there are more commands than colors, the last color will be repeated.\n' +
'- Available modifiers: reset, bold, dim, italic, underline, inverse, hidden, strikethrough\n' +
'- Available colors: black, red, green, yellow, blue, magenta, cyan, white, gray \n' +
'or any hex values for colors, eg #23de43\n' +
'- Available background colors: bgBlack, bgRed, bgGreen, bgYellow, bgBlue, bgMagenta, bgCyan, bgWhite\n' +
'See https://www.npmjs.com/package/chalk for more information.',
default: defaults.prefixColors,
type: 'string'
},
'l': {
alias: 'prefix-length',
describe:
'Limit how many characters of the command is displayed in prefix. ' +
'The option can be used to shorten the prefix when it is set to "command"',
default: defaults.prefixLength,
type: 'number'
},
't': {
alias: 'timestamp-format',
describe: 'Specify the timestamp in moment/date-fns format.',
default: defaults.timestampFormat,
type: 'string'
},
// Restarting
'restart-tries': {
describe:
'How many times a process that died should restart.\n' +
'Negative numbers will make the process restart forever.',
default: defaults.restartTries,
type: 'number'
},
'restart-after': {
describe: 'Delay time to respawn the process, in milliseconds.',
default: defaults.restartDelay,
type: 'number'
},
// Input
'i': {
alias: 'handle-input',
describe:
'Whether input should be forwarded to the child processes. ' +
'See examples for more information.',
type: 'boolean'
},
'default-input-target': {
default: defaults.defaultInputTarget,
describe:
'Identifier for child process to which input on stdin ' +
'should be sent if not specified at start of input.\n' +
'Can be either the index or the name of the process.'
}
})
.group(['m', 'n', 'name-separator', 'raw', 's', 'no-color', 'hide'], 'General')
.group(['p', 'c', 'l', 't'], 'Prefix styling')
.group(['i', 'default-input-target'], 'Input handling')
.group(['k', 'kill-others-on-fail'], 'Killing other processes')
.group(['restart-tries', 'restart-after'], 'Restarting')
// Too much text to write as JS strings, .txt file is better
.epilogue(fs.readFileSync(__dirname + '/epilogue.txt', { encoding: 'utf8' }))
.argv;
const names = (args.names || '').split(args.nameSeparator);
concurrently(args._.map((command, index) => ({
command,
name: names[index]
})), {
handleInput: args.handleInput,
defaultInputTarget: args.defaultInputTarget,
killOthers: args.killOthers
? ['success', 'failure']
: (args.killOthersOnFail ? ['failure'] : []),
maxProcesses: args.maxProcesses,
raw: args.raw,
hide: args.hide.split(','),
prefix: args.prefix,
prefixColors: args.prefixColors.split(','),
prefixLength: args.prefixLength,
restartDelay: args.restartAfter,
restartTries: args.restartTries,
successCondition: args.success,
timestampFormat: args.timestampFormat,
}).then(
() => process.exit(0),
() => process.exit(1)
);

View File

@@ -0,0 +1,385 @@
const readline = require('readline');
const _ = require('lodash');
const Rx = require('rxjs');
const { buffer, map } = require('rxjs/operators');
const spawn = require('spawn-command');
const isWindows = process.platform === 'win32';
const createKillMessage = prefix => new RegExp(
_.escapeRegExp(prefix) +
' exited with code ' +
(isWindows ? 1 : '(SIGTERM|143)')
);
const run = args => {
const child = spawn('node ./concurrently.js ' + args, {
cwd: __dirname,
env: Object.assign({}, process.env, {
// When upgrading from jest 23 -> 24, colors started printing in the test output.
// They are forcibly disabled here
FORCE_COLOR: 0
}),
});
const stdout = readline.createInterface({
input: child.stdout,
output: null
});
const stderr = readline.createInterface({
input: child.stderr,
output: null
});
const close = Rx.fromEvent(child, 'close');
const log = Rx.merge(
Rx.fromEvent(stdout, 'line'),
Rx.fromEvent(stderr, 'line')
).pipe(map(data => data.toString()));
return {
close,
log,
stdin: child.stdin,
pid: child.pid
};
};
it('has help command', done => {
run('--help').close.subscribe(event => {
expect(event[0]).toBe(0);
done();
}, done);
});
it('has version command', done => {
Rx.combineLatest(
run('--version').close,
run('-V').close,
run('-v').close
).subscribe(events => {
expect(events[0][0]).toBe(0);
expect(events[1][0]).toBe(0);
expect(events[2][0]).toBe(0);
done();
}, done);
});
describe('exiting conditions', () => {
it('is of success by default when running successful commands', done => {
run('"echo foo" "echo bar"')
.close
.subscribe(exit => {
expect(exit[0]).toBe(0);
done();
}, done);
});
it('is of failure by default when one of the command fails', done => {
run('"echo foo" "exit 1"')
.close
.subscribe(exit => {
expect(exit[0]).toBeGreaterThan(0);
done();
}, done);
});
it('is of success when --success=first and first command to exit succeeds', done => {
run('--success=first "echo foo" "sleep 0.5 && exit 1"')
.close
.subscribe(exit => {
expect(exit[0]).toBe(0);
done();
}, done);
});
it('is of failure when --success=first and first command to exit fails', done => {
run('--success=first "exit 1" "sleep 0.5 && echo foo"')
.close
.subscribe(exit => {
expect(exit[0]).toBeGreaterThan(0);
done();
}, done);
});
it('is of success when --success=last and last command to exit succeeds', done => {
run('--success=last "exit 1" "sleep 0.5 && echo foo"')
.close
.subscribe(exit => {
expect(exit[0]).toBe(0);
done();
}, done);
});
it('is of failure when --success=last and last command to exit fails', done => {
run('--success=last "echo foo" "sleep 0.5 && exit 1"')
.close
.subscribe(exit => {
expect(exit[0]).toBeGreaterThan(0);
done();
}, done);
});
it.skip('is of success when a SIGINT is sent', done => {
const child = run('"node fixtures/read-echo.js"');
child.close.subscribe(exit => {
// TODO This is null within Node, but should be 0 outside (eg from real terminal)
expect(exit[0]).toBe(0);
done();
}, done);
process.kill(child.pid, 'SIGINT');
});
it('is aliased to -s', done => {
run('-s last "exit 1" "sleep 0.5 && echo foo"')
.close
.subscribe(exit => {
expect(exit[0]).toBe(0);
done();
}, done);
});
});
describe('--raw', () => {
it('is aliased to -r', done => {
const child = run('-r "echo foo" "echo bar"');
child.log.pipe(buffer(child.close)).subscribe(lines => {
expect(lines).toHaveLength(2);
expect(lines).toContainEqual(expect.stringContaining('foo'));
expect(lines).toContainEqual(expect.stringContaining('bar'));
done();
}, done);
});
it('does not log any extra output', done => {
const child = run('--raw "echo foo" "echo bar"');
child.log.pipe(buffer(child.close)).subscribe(lines => {
expect(lines).toHaveLength(2);
expect(lines).toContainEqual(expect.stringContaining('foo'));
expect(lines).toContainEqual(expect.stringContaining('bar'));
done();
}, done);
});
});
describe('--hide', () => {
it('hides the output of a process by its index', done => {
const child = run('--hide 1 "echo foo" "echo bar"');
child.log.pipe(buffer(child.close)).subscribe(lines => {
expect(lines).toContainEqual(expect.stringContaining('foo'));
expect(lines).not.toContainEqual(expect.stringContaining('bar'));
done();
}, done);
});
it('hides the output of a process by its name', done => {
const child = run('-n foo,bar --hide bar "echo foo" "echo bar"');
child.log.pipe(buffer(child.close)).subscribe(lines => {
expect(lines).toContainEqual(expect.stringContaining('foo'));
expect(lines).not.toContainEqual(expect.stringContaining('bar'));
done();
}, done);
});
});
describe('--names', () => {
it('is aliased to -n', done => {
const child = run('-n foo,bar "echo foo" "echo bar"');
child.log.pipe(buffer(child.close)).subscribe(lines => {
expect(lines).toContainEqual(expect.stringContaining('[foo] foo'));
expect(lines).toContainEqual(expect.stringContaining('[bar] bar'));
done();
}, done);
});
it('prefixes with names', done => {
const child = run('--names foo,bar "echo foo" "echo bar"');
child.log.pipe(buffer(child.close)).subscribe(lines => {
expect(lines).toContainEqual(expect.stringContaining('[foo] foo'));
expect(lines).toContainEqual(expect.stringContaining('[bar] bar'));
done();
}, done);
});
it('is split using --name-separator arg', done => {
const child = run('--names "foo|bar" --name-separator "|" "echo foo" "echo bar"');
child.log.pipe(buffer(child.close)).subscribe(lines => {
expect(lines).toContainEqual(expect.stringContaining('[foo] foo'));
expect(lines).toContainEqual(expect.stringContaining('[bar] bar'));
done();
}, done);
});
});
describe('--prefix', () => {
it('is aliased to -p', done => {
const child = run('-p command "echo foo" "echo bar"');
child.log.pipe(buffer(child.close)).subscribe(lines => {
expect(lines).toContainEqual(expect.stringContaining('[echo foo] foo'));
expect(lines).toContainEqual(expect.stringContaining('[echo bar] bar'));
done();
}, done);
});
it('specifies custom prefix', done => {
const child = run('--prefix command "echo foo" "echo bar"');
child.log.pipe(buffer(child.close)).subscribe(lines => {
expect(lines).toContainEqual(expect.stringContaining('[echo foo] foo'));
expect(lines).toContainEqual(expect.stringContaining('[echo bar] bar'));
done();
}, done);
});
});
describe('--prefix-length', () => {
it('is aliased to -l', done => {
const child = run('-p command -l 5 "echo foo" "echo bar"');
child.log.pipe(buffer(child.close)).subscribe(lines => {
expect(lines).toContainEqual(expect.stringContaining('[ec..o] foo'));
expect(lines).toContainEqual(expect.stringContaining('[ec..r] bar'));
done();
}, done);
});
it('specifies custom prefix length', done => {
const child = run('--prefix command --prefix-length 5 "echo foo" "echo bar"');
child.log.pipe(buffer(child.close)).subscribe(lines => {
expect(lines).toContainEqual(expect.stringContaining('[ec..o] foo'));
expect(lines).toContainEqual(expect.stringContaining('[ec..r] bar'));
done();
}, done);
});
});
describe('--restart-tries', () => {
it('changes how many times a command will restart', done => {
const child = run('--restart-tries 1 "exit 1"');
child.log.pipe(buffer(child.close)).subscribe(lines => {
expect(lines).toEqual([
expect.stringContaining('[0] exit 1 exited with code 1'),
expect.stringContaining('[0] exit 1 restarted'),
expect.stringContaining('[0] exit 1 exited with code 1'),
]);
done();
}, done);
});
});
describe('--kill-others', () => {
it('is aliased to -k', done => {
const child = run('-k "sleep 10" "exit 0"');
child.log.pipe(buffer(child.close)).subscribe(lines => {
expect(lines).toContainEqual(expect.stringContaining('[1] exit 0 exited with code 0'));
expect(lines).toContainEqual(expect.stringContaining('Sending SIGTERM to other processes'));
expect(lines).toContainEqual(expect.stringMatching(createKillMessage('[0] sleep 10')));
done();
}, done);
});
it('kills on success', done => {
const child = run('--kill-others "sleep 10" "exit 0"');
child.log.pipe(buffer(child.close)).subscribe(lines => {
expect(lines).toContainEqual(expect.stringContaining('[1] exit 0 exited with code 0'));
expect(lines).toContainEqual(expect.stringContaining('Sending SIGTERM to other processes'));
expect(lines).toContainEqual(expect.stringMatching(createKillMessage('[0] sleep 10')));
done();
}, done);
});
it('kills on failure', done => {
const child = run('--kill-others "sleep 10" "exit 1"');
child.log.pipe(buffer(child.close)).subscribe(lines => {
expect(lines).toContainEqual(expect.stringContaining('[1] exit 1 exited with code 1'));
expect(lines).toContainEqual(expect.stringContaining('Sending SIGTERM to other processes'));
expect(lines).toContainEqual(expect.stringMatching(createKillMessage('[0] sleep 10')));
done();
}, done);
});
});
describe('--kill-others-on-fail', () => {
it('does not kill on success', done => {
const child = run('--kill-others-on-fail "sleep 0.5" "exit 0"');
child.log.pipe(buffer(child.close)).subscribe(lines => {
expect(lines).toContainEqual(expect.stringContaining('[1] exit 0 exited with code 0'));
expect(lines).toContainEqual(expect.stringContaining('[0] sleep 0.5 exited with code 0'));
done();
}, done);
});
it('kills on failure', done => {
const child = run('--kill-others-on-fail "sleep 10" "exit 1"');
child.log.pipe(buffer(child.close)).subscribe(lines => {
expect(lines).toContainEqual(expect.stringContaining('[1] exit 1 exited with code 1'));
expect(lines).toContainEqual(expect.stringContaining('Sending SIGTERM to other processes'));
expect(lines).toContainEqual(expect.stringMatching(createKillMessage('[0] sleep 10')));
done();
}, done);
});
});
describe('--handle-input', () => {
it('is aliased to -i', done => {
const child = run('-i "node fixtures/read-echo.js"');
child.log.subscribe(line => {
if (/READING/.test(line)) {
child.stdin.write('stop\n');
}
if (/\[0\] stop/.test(line)) {
done();
}
}, done);
});
it('forwards input to first process by default', done => {
const child = run('--handle-input "node fixtures/read-echo.js"');
child.log.subscribe(line => {
if (/READING/.test(line)) {
child.stdin.write('stop\n');
}
if (/\[0\] stop/.test(line)) {
done();
}
}, done);
});
it('forwards input to process --default-input-target', done => {
const lines = [];
const child = run('-ki --default-input-target 1 "node fixtures/read-echo.js" "node fixtures/read-echo.js"');
child.log.subscribe(line => {
lines.push(line);
if (/\[1\] READING/.test(line)) {
child.stdin.write('stop\n');
}
}, done);
child.close.subscribe(exit => {
expect(exit[0]).toBeGreaterThan(0);
expect(lines).toContainEqual(expect.stringContaining('[1] stop'));
expect(lines).toContainEqual(expect.stringMatching(createKillMessage('[0] node fixtures/read-echo.js')));
done();
}, done);
});
it('forwards input to specified process', done => {
const lines = [];
const child = run('-ki "node fixtures/read-echo.js" "node fixtures/read-echo.js"');
child.log.subscribe(line => {
lines.push(line);
if (/\[1\] READING/.test(line)) {
child.stdin.write('1:stop\n');
}
}, done);
child.close.subscribe(exit => {
expect(exit[0]).toBeGreaterThan(0);
expect(lines).toContainEqual(expect.stringContaining('[1] stop'));
expect(lines).toContainEqual(expect.stringMatching(createKillMessage('[0] node fixtures/read-echo.js')));
done();
}, done);
});
});

View File

@@ -0,0 +1,42 @@
Examples:
- Output nothing more than stdout+stderr of child processes
$ $0 --raw "npm run watch-less" "npm run watch-js"
- Normal output but without colors e.g. when logging to file
$ $0 --no-color "grunt watch" "http-server" > log
- Custom prefix
$ $0 --prefix "{time}-{pid}" "npm run watch" "http-server"
- Custom names and colored prefixes
$ $0 --names "HTTP,WATCH" -c "bgBlue.bold,bgMagenta.bold" "http-server" "npm run watch"
- Send input to default
$ $0 --handle-input "nodemon" "npm run watch-js"
rs # Sends rs command to nodemon process
- Send input to specific child identified by index
$ $0 --handle-input "npm run watch-js" nodemon
1:rs
- Send input to specific child identified by name
$ $0 --handle-input -n js,srv "npm run watch-js" nodemon
srv:rs
- Shortened NPM run commands
$ $0 npm:watch-node npm:watch-js npm:watch-css
- Shortened NPM run command with wildcard
$ $0 npm:watch-*
For more details, visit https://github.com/open-cli-tools/concurrently

62
vendor/spatie/ignition/node_modules/concurrently/index.js generated vendored Executable file
View File

@@ -0,0 +1,62 @@
const InputHandler = require('./src/flow-control/input-handler');
const KillOnSignal = require('./src/flow-control/kill-on-signal');
const KillOthers = require('./src/flow-control/kill-others');
const LogError = require('./src/flow-control/log-error');
const LogExit = require('./src/flow-control/log-exit');
const LogOutput = require('./src/flow-control/log-output');
const RestartProcess = require('./src/flow-control/restart-process');
const concurrently = require('./src/concurrently');
const Logger = require('./src/logger');
module.exports = exports = (commands, options = {}) => {
const logger = new Logger({
hide: options.hide,
outputStream: options.outputStream || process.stdout,
prefixFormat: options.prefix,
prefixLength: options.prefixLength,
raw: options.raw,
timestampFormat: options.timestampFormat,
});
return concurrently(commands, {
maxProcesses: options.maxProcesses,
raw: options.raw,
successCondition: options.successCondition,
cwd: options.cwd,
controllers: [
new LogError({ logger }),
new LogOutput({ logger }),
new LogExit({ logger }),
new InputHandler({
logger,
defaultInputTarget: options.defaultInputTarget,
inputStream: options.inputStream || (options.handleInput && process.stdin),
pauseInputStreamOnFinish: options.pauseInputStreamOnFinish,
}),
new KillOnSignal({ process }),
new RestartProcess({
logger,
delay: options.restartDelay,
tries: options.restartTries,
}),
new KillOthers({
logger,
conditions: options.killOthers
})
],
prefixColors: options.prefixColors || []
});
};
// Export all flow controllers and the main concurrently function,
// so that 3rd-parties can use them however they want
exports.concurrently = concurrently;
exports.Logger = Logger;
exports.InputHandler = InputHandler;
exports.KillOnSignal = KillOnSignal;
exports.KillOthers = KillOthers;
exports.LogError = LogError;
exports.LogExit = LogExit;
exports.LogOutput = LogOutput;
exports.RestartProcess = RestartProcess;

View File

@@ -0,0 +1 @@
../../../tree-kill/cli.js

View File

@@ -0,0 +1,415 @@
/**
Basic foreground colors.
[More colors here.](https://github.com/chalk/chalk/blob/master/readme.md#256-and-truecolor-color-support)
*/
declare type ForegroundColor =
| 'black'
| 'red'
| 'green'
| 'yellow'
| 'blue'
| 'magenta'
| 'cyan'
| 'white'
| 'gray'
| 'grey'
| 'blackBright'
| 'redBright'
| 'greenBright'
| 'yellowBright'
| 'blueBright'
| 'magentaBright'
| 'cyanBright'
| 'whiteBright';
/**
Basic background colors.
[More colors here.](https://github.com/chalk/chalk/blob/master/readme.md#256-and-truecolor-color-support)
*/
declare type BackgroundColor =
| 'bgBlack'
| 'bgRed'
| 'bgGreen'
| 'bgYellow'
| 'bgBlue'
| 'bgMagenta'
| 'bgCyan'
| 'bgWhite'
| 'bgGray'
| 'bgGrey'
| 'bgBlackBright'
| 'bgRedBright'
| 'bgGreenBright'
| 'bgYellowBright'
| 'bgBlueBright'
| 'bgMagentaBright'
| 'bgCyanBright'
| 'bgWhiteBright';
/**
Basic colors.
[More colors here.](https://github.com/chalk/chalk/blob/master/readme.md#256-and-truecolor-color-support)
*/
declare type Color = ForegroundColor | BackgroundColor;
declare type Modifiers =
| 'reset'
| 'bold'
| 'dim'
| 'italic'
| 'underline'
| 'inverse'
| 'hidden'
| 'strikethrough'
| 'visible';
declare namespace chalk {
/**
Levels:
- `0` - All colors disabled.
- `1` - Basic 16 colors support.
- `2` - ANSI 256 colors support.
- `3` - Truecolor 16 million colors support.
*/
type Level = 0 | 1 | 2 | 3;
interface Options {
/**
Specify the color support for Chalk.
By default, color support is automatically detected based on the environment.
Levels:
- `0` - All colors disabled.
- `1` - Basic 16 colors support.
- `2` - ANSI 256 colors support.
- `3` - Truecolor 16 million colors support.
*/
level?: Level;
}
/**
Return a new Chalk instance.
*/
type Instance = new (options?: Options) => Chalk;
/**
Detect whether the terminal supports color.
*/
interface ColorSupport {
/**
The color level used by Chalk.
*/
level: Level;
/**
Return whether Chalk supports basic 16 colors.
*/
hasBasic: boolean;
/**
Return whether Chalk supports ANSI 256 colors.
*/
has256: boolean;
/**
Return whether Chalk supports Truecolor 16 million colors.
*/
has16m: boolean;
}
interface ChalkFunction {
/**
Use a template string.
@remarks Template literals are unsupported for nested calls (see [issue #341](https://github.com/chalk/chalk/issues/341))
@example
```
import chalk = require('chalk');
log(chalk`
CPU: {red ${cpu.totalPercent}%}
RAM: {green ${ram.used / ram.total * 100}%}
DISK: {rgb(255,131,0) ${disk.used / disk.total * 100}%}
`);
```
@example
```
import chalk = require('chalk');
log(chalk.red.bgBlack`2 + 3 = {bold ${2 + 3}}`)
```
*/
(text: TemplateStringsArray, ...placeholders: unknown[]): string;
(...text: unknown[]): string;
}
interface Chalk extends ChalkFunction {
/**
Return a new Chalk instance.
*/
Instance: Instance;
/**
The color support for Chalk.
By default, color support is automatically detected based on the environment.
Levels:
- `0` - All colors disabled.
- `1` - Basic 16 colors support.
- `2` - ANSI 256 colors support.
- `3` - Truecolor 16 million colors support.
*/
level: Level;
/**
Use HEX value to set text color.
@param color - Hexadecimal value representing the desired color.
@example
```
import chalk = require('chalk');
chalk.hex('#DEADED');
```
*/
hex(color: string): Chalk;
/**
Use keyword color value to set text color.
@param color - Keyword value representing the desired color.
@example
```
import chalk = require('chalk');
chalk.keyword('orange');
```
*/
keyword(color: string): Chalk;
/**
Use RGB values to set text color.
*/
rgb(red: number, green: number, blue: number): Chalk;
/**
Use HSL values to set text color.
*/
hsl(hue: number, saturation: number, lightness: number): Chalk;
/**
Use HSV values to set text color.
*/
hsv(hue: number, saturation: number, value: number): Chalk;
/**
Use HWB values to set text color.
*/
hwb(hue: number, whiteness: number, blackness: number): Chalk;
/**
Use a [Select/Set Graphic Rendition](https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters) (SGR) [color code number](https://en.wikipedia.org/wiki/ANSI_escape_code#3/4_bit) to set text color.
30 <= code && code < 38 || 90 <= code && code < 98
For example, 31 for red, 91 for redBright.
*/
ansi(code: number): Chalk;
/**
Use a [8-bit unsigned number](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit) to set text color.
*/
ansi256(index: number): Chalk;
/**
Use HEX value to set background color.
@param color - Hexadecimal value representing the desired color.
@example
```
import chalk = require('chalk');
chalk.bgHex('#DEADED');
```
*/
bgHex(color: string): Chalk;
/**
Use keyword color value to set background color.
@param color - Keyword value representing the desired color.
@example
```
import chalk = require('chalk');
chalk.bgKeyword('orange');
```
*/
bgKeyword(color: string): Chalk;
/**
Use RGB values to set background color.
*/
bgRgb(red: number, green: number, blue: number): Chalk;
/**
Use HSL values to set background color.
*/
bgHsl(hue: number, saturation: number, lightness: number): Chalk;
/**
Use HSV values to set background color.
*/
bgHsv(hue: number, saturation: number, value: number): Chalk;
/**
Use HWB values to set background color.
*/
bgHwb(hue: number, whiteness: number, blackness: number): Chalk;
/**
Use a [Select/Set Graphic Rendition](https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters) (SGR) [color code number](https://en.wikipedia.org/wiki/ANSI_escape_code#3/4_bit) to set background color.
30 <= code && code < 38 || 90 <= code && code < 98
For example, 31 for red, 91 for redBright.
Use the foreground code, not the background code (for example, not 41, nor 101).
*/
bgAnsi(code: number): Chalk;
/**
Use a [8-bit unsigned number](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit) to set background color.
*/
bgAnsi256(index: number): Chalk;
/**
Modifier: Resets the current color chain.
*/
readonly reset: Chalk;
/**
Modifier: Make text bold.
*/
readonly bold: Chalk;
/**
Modifier: Emitting only a small amount of light.
*/
readonly dim: Chalk;
/**
Modifier: Make text italic. (Not widely supported)
*/
readonly italic: Chalk;
/**
Modifier: Make text underline. (Not widely supported)
*/
readonly underline: Chalk;
/**
Modifier: Inverse background and foreground colors.
*/
readonly inverse: Chalk;
/**
Modifier: Prints the text, but makes it invisible.
*/
readonly hidden: Chalk;
/**
Modifier: Puts a horizontal line through the center of the text. (Not widely supported)
*/
readonly strikethrough: Chalk;
/**
Modifier: Prints the text only when Chalk has a color support level > 0.
Can be useful for things that are purely cosmetic.
*/
readonly visible: Chalk;
readonly black: Chalk;
readonly red: Chalk;
readonly green: Chalk;
readonly yellow: Chalk;
readonly blue: Chalk;
readonly magenta: Chalk;
readonly cyan: Chalk;
readonly white: Chalk;
/*
Alias for `blackBright`.
*/
readonly gray: Chalk;
/*
Alias for `blackBright`.
*/
readonly grey: Chalk;
readonly blackBright: Chalk;
readonly redBright: Chalk;
readonly greenBright: Chalk;
readonly yellowBright: Chalk;
readonly blueBright: Chalk;
readonly magentaBright: Chalk;
readonly cyanBright: Chalk;
readonly whiteBright: Chalk;
readonly bgBlack: Chalk;
readonly bgRed: Chalk;
readonly bgGreen: Chalk;
readonly bgYellow: Chalk;
readonly bgBlue: Chalk;
readonly bgMagenta: Chalk;
readonly bgCyan: Chalk;
readonly bgWhite: Chalk;
/*
Alias for `bgBlackBright`.
*/
readonly bgGray: Chalk;
/*
Alias for `bgBlackBright`.
*/
readonly bgGrey: Chalk;
readonly bgBlackBright: Chalk;
readonly bgRedBright: Chalk;
readonly bgGreenBright: Chalk;
readonly bgYellowBright: Chalk;
readonly bgBlueBright: Chalk;
readonly bgMagentaBright: Chalk;
readonly bgCyanBright: Chalk;
readonly bgWhiteBright: Chalk;
}
}
/**
Main Chalk object that allows to chain styles together.
Call the last one as a method with a string argument.
Order doesn't matter, and later styles take precedent in case of a conflict.
This simply means that `chalk.red.yellow.green` is equivalent to `chalk.green`.
*/
declare const chalk: chalk.Chalk & chalk.ChalkFunction & {
supportsColor: chalk.ColorSupport | false;
Level: chalk.Level;
Color: Color;
ForegroundColor: ForegroundColor;
BackgroundColor: BackgroundColor;
Modifiers: Modifiers;
stderr: chalk.Chalk & {supportsColor: chalk.ColorSupport | false};
};
export = chalk;

View File

@@ -0,0 +1,9 @@
MIT License
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = {
stdout: false,
stderr: false
};

View File

@@ -0,0 +1,135 @@
'use strict';
const os = require('os');
const tty = require('tty');
const hasFlag = require('has-flag');
const {env} = process;
let forceColor;
if (hasFlag('no-color') ||
hasFlag('no-colors') ||
hasFlag('color=false') ||
hasFlag('color=never')) {
forceColor = 0;
} else if (hasFlag('color') ||
hasFlag('colors') ||
hasFlag('color=true') ||
hasFlag('color=always')) {
forceColor = 1;
}
if ('FORCE_COLOR' in env) {
if (env.FORCE_COLOR === 'true') {
forceColor = 1;
} else if (env.FORCE_COLOR === 'false') {
forceColor = 0;
} else {
forceColor = env.FORCE_COLOR.length === 0 ? 1 : Math.min(parseInt(env.FORCE_COLOR, 10), 3);
}
}
function translateLevel(level) {
if (level === 0) {
return false;
}
return {
level,
hasBasic: true,
has256: level >= 2,
has16m: level >= 3
};
}
function supportsColor(haveStream, streamIsTTY) {
if (forceColor === 0) {
return 0;
}
if (hasFlag('color=16m') ||
hasFlag('color=full') ||
hasFlag('color=truecolor')) {
return 3;
}
if (hasFlag('color=256')) {
return 2;
}
if (haveStream && !streamIsTTY && forceColor === undefined) {
return 0;
}
const min = forceColor || 0;
if (env.TERM === 'dumb') {
return min;
}
if (process.platform === 'win32') {
// Windows 10 build 10586 is the first Windows release that supports 256 colors.
// Windows 10 build 14931 is the first release that supports 16m/TrueColor.
const osRelease = os.release().split('.');
if (
Number(osRelease[0]) >= 10 &&
Number(osRelease[2]) >= 10586
) {
return Number(osRelease[2]) >= 14931 ? 3 : 2;
}
return 1;
}
if ('CI' in env) {
if (['TRAVIS', 'CIRCLECI', 'APPVEYOR', 'GITLAB_CI', 'GITHUB_ACTIONS', 'BUILDKITE'].some(sign => sign in env) || env.CI_NAME === 'codeship') {
return 1;
}
return min;
}
if ('TEAMCITY_VERSION' in env) {
return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(env.TEAMCITY_VERSION) ? 1 : 0;
}
if (env.COLORTERM === 'truecolor') {
return 3;
}
if ('TERM_PROGRAM' in env) {
const version = parseInt((env.TERM_PROGRAM_VERSION || '').split('.')[0], 10);
switch (env.TERM_PROGRAM) {
case 'iTerm.app':
return version >= 3 ? 3 : 2;
case 'Apple_Terminal':
return 2;
// No default
}
}
if (/-256(color)?$/i.test(env.TERM)) {
return 2;
}
if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(env.TERM)) {
return 1;
}
if ('COLORTERM' in env) {
return 1;
}
return min;
}
function getSupportLevel(stream) {
const level = supportsColor(stream, stream && stream.isTTY);
return translateLevel(level);
}
module.exports = {
supportsColor: getSupportLevel,
stdout: translateLevel(supportsColor(true, tty.isatty(1))),
stderr: translateLevel(supportsColor(true, tty.isatty(2)))
};

View File

@@ -0,0 +1,9 @@
MIT License
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,76 @@
# supports-color [![Build Status](https://travis-ci.org/chalk/supports-color.svg?branch=master)](https://travis-ci.org/chalk/supports-color)
> Detect whether a terminal supports color
## Install
```
$ npm install supports-color
```
## Usage
```js
const supportsColor = require('supports-color');
if (supportsColor.stdout) {
console.log('Terminal stdout supports color');
}
if (supportsColor.stdout.has256) {
console.log('Terminal stdout supports 256 colors');
}
if (supportsColor.stderr.has16m) {
console.log('Terminal stderr supports 16 million colors (truecolor)');
}
```
## API
Returns an `Object` with a `stdout` and `stderr` property for testing either streams. Each property is an `Object`, or `false` if color is not supported.
The `stdout`/`stderr` objects specifies a level of support for color through a `.level` property and a corresponding flag:
- `.level = 1` and `.hasBasic = true`: Basic color support (16 colors)
- `.level = 2` and `.has256 = true`: 256 color support
- `.level = 3` and `.has16m = true`: Truecolor support (16 million colors)
## Info
It obeys the `--color` and `--no-color` CLI flags.
For situations where using `--color` is not possible, use the environment variable `FORCE_COLOR=1` (level 1), `FORCE_COLOR=2` (level 2), or `FORCE_COLOR=3` (level 3) to forcefully enable color, or `FORCE_COLOR=0` to forcefully disable. The use of `FORCE_COLOR` overrides all other color support checks.
Explicit 256/Truecolor mode can be enabled using the `--color=256` and `--color=16m` flags, respectively.
## Related
- [supports-color-cli](https://github.com/chalk/supports-color-cli) - CLI for this module
- [chalk](https://github.com/chalk/chalk) - Terminal string styling done right
## Maintainers
- [Sindre Sorhus](https://github.com/sindresorhus)
- [Josh Junon](https://github.com/qix-)
---
<div align="center">
<b>
<a href="https://tidelift.com/subscription/pkg/npm-supports-color?utm_source=npm-supports-color&utm_medium=referral&utm_campaign=readme">Get professional support for this package with a Tidelift subscription</a>
</b>
<br>
<sub>
Tidelift helps make open source sustainable for maintainers while giving companies<br>assurances about security, maintenance, and licensing for their dependencies.
</sub>
</div>
---

View File

@@ -0,0 +1,335 @@
<h1 align="center">
<br>
<br>
<img width="320" src="media/logo.svg" alt="Chalk">
<br>
<br>
<br>
</h1>
> Terminal string styling done right
[![Build Status](https://travis-ci.org/chalk/chalk.svg?branch=master)](https://travis-ci.org/chalk/chalk) [![Coverage Status](https://coveralls.io/repos/github/chalk/chalk/badge.svg?branch=master)](https://coveralls.io/github/chalk/chalk?branch=master) [![npm dependents](https://badgen.net/npm/dependents/chalk)](https://www.npmjs.com/package/chalk?activeTab=dependents) [![Downloads](https://badgen.net/npm/dt/chalk)](https://www.npmjs.com/package/chalk) [![](https://img.shields.io/badge/unicorn-approved-ff69b4.svg)](https://www.youtube.com/watch?v=9auOCbH5Ns4) [![XO code style](https://img.shields.io/badge/code_style-XO-5ed9c7.svg)](https://github.com/xojs/xo) ![TypeScript-ready](https://img.shields.io/npm/types/chalk.svg) [![run on repl.it](https://repl.it/badge/github/chalk/chalk)](https://repl.it/github/chalk/chalk)
<img src="https://cdn.jsdelivr.net/gh/chalk/ansi-styles@8261697c95bf34b6c7767e2cbe9941a851d59385/screenshot.svg" width="900">
<br>
---
<div align="center">
<p>
<p>
<sup>
Sindre Sorhus' open source work is supported by the community on <a href="https://github.com/sponsors/sindresorhus">GitHub Sponsors</a> and <a href="https://stakes.social/0x44d871aebF0126Bf646753E2C976Aa7e68A66c15">Dev</a>
</sup>
</p>
<sup>Special thanks to:</sup>
<br>
<br>
<a href="https://standardresume.co/tech">
<img src="https://sindresorhus.com/assets/thanks/standard-resume-logo.svg" width="160"/>
</a>
<br>
<br>
<a href="https://retool.com/?utm_campaign=sindresorhus">
<img src="https://sindresorhus.com/assets/thanks/retool-logo.svg" width="210"/>
</a>
<br>
<br>
<a href="https://doppler.com/?utm_campaign=github_repo&utm_medium=referral&utm_content=chalk&utm_source=github">
<div>
<img src="https://dashboard.doppler.com/imgs/logo-long.svg" width="240" alt="Doppler">
</div>
<b>All your environment variables, in one place</b>
<div>
<span>Stop struggling with scattered API keys, hacking together home-brewed tools,</span>
<br>
<span>and avoiding access controls. Keep your team and servers in sync with Doppler.</span>
</div>
</a>
</p>
</div>
---
<br>
## Highlights
- Expressive API
- Highly performant
- Ability to nest styles
- [256/Truecolor color support](#256-and-truecolor-color-support)
- Auto-detects color support
- Doesn't extend `String.prototype`
- Clean and focused
- Actively maintained
- [Used by ~50,000 packages](https://www.npmjs.com/browse/depended/chalk) as of January 1, 2020
## Install
```console
$ npm install chalk
```
## Usage
```js
const chalk = require('chalk');
console.log(chalk.blue('Hello world!'));
```
Chalk comes with an easy to use composable API where you just chain and nest the styles you want.
```js
const chalk = require('chalk');
const log = console.log;
// Combine styled and normal strings
log(chalk.blue('Hello') + ' World' + chalk.red('!'));
// Compose multiple styles using the chainable API
log(chalk.blue.bgRed.bold('Hello world!'));
// Pass in multiple arguments
log(chalk.blue('Hello', 'World!', 'Foo', 'bar', 'biz', 'baz'));
// Nest styles
log(chalk.red('Hello', chalk.underline.bgBlue('world') + '!'));
// Nest styles of the same type even (color, underline, background)
log(chalk.green(
'I am a green line ' +
chalk.blue.underline.bold('with a blue substring') +
' that becomes green again!'
));
// ES2015 template literal
log(`
CPU: ${chalk.red('90%')}
RAM: ${chalk.green('40%')}
DISK: ${chalk.yellow('70%')}
`);
// ES2015 tagged template literal
log(chalk`
CPU: {red ${cpu.totalPercent}%}
RAM: {green ${ram.used / ram.total * 100}%}
DISK: {rgb(255,131,0) ${disk.used / disk.total * 100}%}
`);
// Use RGB colors in terminal emulators that support it.
log(chalk.keyword('orange')('Yay for orange colored text!'));
log(chalk.rgb(123, 45, 67).underline('Underlined reddish color'));
log(chalk.hex('#DEADED').bold('Bold gray!'));
```
Easily define your own themes:
```js
const chalk = require('chalk');
const error = chalk.bold.red;
const warning = chalk.keyword('orange');
console.log(error('Error!'));
console.log(warning('Warning!'));
```
Take advantage of console.log [string substitution](https://nodejs.org/docs/latest/api/console.html#console_console_log_data_args):
```js
const name = 'Sindre';
console.log(chalk.green('Hello %s'), name);
//=> 'Hello Sindre'
```
## API
### chalk.`<style>[.<style>...](string, [string...])`
Example: `chalk.red.bold.underline('Hello', 'world');`
Chain [styles](#styles) and call the last one as a method with a string argument. Order doesn't matter, and later styles take precedent in case of a conflict. This simply means that `chalk.red.yellow.green` is equivalent to `chalk.green`.
Multiple arguments will be separated by space.
### chalk.level
Specifies the level of color support.
Color support is automatically detected, but you can override it by setting the `level` property. You should however only do this in your own code as it applies globally to all Chalk consumers.
If you need to change this in a reusable module, create a new instance:
```js
const ctx = new chalk.Instance({level: 0});
```
| Level | Description |
| :---: | :--- |
| `0` | All colors disabled |
| `1` | Basic color support (16 colors) |
| `2` | 256 color support |
| `3` | Truecolor support (16 million colors) |
### chalk.supportsColor
Detect whether the terminal [supports color](https://github.com/chalk/supports-color). Used internally and handled for you, but exposed for convenience.
Can be overridden by the user with the flags `--color` and `--no-color`. For situations where using `--color` is not possible, use the environment variable `FORCE_COLOR=1` (level 1), `FORCE_COLOR=2` (level 2), or `FORCE_COLOR=3` (level 3) to forcefully enable color, or `FORCE_COLOR=0` to forcefully disable. The use of `FORCE_COLOR` overrides all other color support checks.
Explicit 256/Truecolor mode can be enabled using the `--color=256` and `--color=16m` flags, respectively.
### chalk.stderr and chalk.stderr.supportsColor
`chalk.stderr` contains a separate instance configured with color support detected for `stderr` stream instead of `stdout`. Override rules from `chalk.supportsColor` apply to this too. `chalk.stderr.supportsColor` is exposed for convenience.
## Styles
### Modifiers
- `reset` - Resets the current color chain.
- `bold` - Make text bold.
- `dim` - Emitting only a small amount of light.
- `italic` - Make text italic. *(Not widely supported)*
- `underline` - Make text underline. *(Not widely supported)*
- `inverse`- Inverse background and foreground colors.
- `hidden` - Prints the text, but makes it invisible.
- `strikethrough` - Puts a horizontal line through the center of the text. *(Not widely supported)*
- `visible`- Prints the text only when Chalk has a color level > 0. Can be useful for things that are purely cosmetic.
### Colors
- `black`
- `red`
- `green`
- `yellow`
- `blue`
- `magenta`
- `cyan`
- `white`
- `blackBright` (alias: `gray`, `grey`)
- `redBright`
- `greenBright`
- `yellowBright`
- `blueBright`
- `magentaBright`
- `cyanBright`
- `whiteBright`
### Background colors
- `bgBlack`
- `bgRed`
- `bgGreen`
- `bgYellow`
- `bgBlue`
- `bgMagenta`
- `bgCyan`
- `bgWhite`
- `bgBlackBright` (alias: `bgGray`, `bgGrey`)
- `bgRedBright`
- `bgGreenBright`
- `bgYellowBright`
- `bgBlueBright`
- `bgMagentaBright`
- `bgCyanBright`
- `bgWhiteBright`
## Tagged template literal
Chalk can be used as a [tagged template literal](https://exploringjs.com/es6/ch_template-literals.html#_tagged-template-literals).
```js
const chalk = require('chalk');
const miles = 18;
const calculateFeet = miles => miles * 5280;
console.log(chalk`
There are {bold 5280 feet} in a mile.
In {bold ${miles} miles}, there are {green.bold ${calculateFeet(miles)} feet}.
`);
```
Blocks are delimited by an opening curly brace (`{`), a style, some content, and a closing curly brace (`}`).
Template styles are chained exactly like normal Chalk styles. The following three statements are equivalent:
```js
console.log(chalk.bold.rgb(10, 100, 200)('Hello!'));
console.log(chalk.bold.rgb(10, 100, 200)`Hello!`);
console.log(chalk`{bold.rgb(10,100,200) Hello!}`);
```
Note that function styles (`rgb()`, `hsl()`, `keyword()`, etc.) may not contain spaces between parameters.
All interpolated values (`` chalk`${foo}` ``) are converted to strings via the `.toString()` method. All curly braces (`{` and `}`) in interpolated value strings are escaped.
## 256 and Truecolor color support
Chalk supports 256 colors and [Truecolor](https://gist.github.com/XVilka/8346728) (16 million colors) on supported terminal apps.
Colors are downsampled from 16 million RGB values to an ANSI color format that is supported by the terminal emulator (or by specifying `{level: n}` as a Chalk option). For example, Chalk configured to run at level 1 (basic color support) will downsample an RGB value of #FF0000 (red) to 31 (ANSI escape for red).
Examples:
- `chalk.hex('#DEADED').underline('Hello, world!')`
- `chalk.keyword('orange')('Some orange text')`
- `chalk.rgb(15, 100, 204).inverse('Hello!')`
Background versions of these models are prefixed with `bg` and the first level of the module capitalized (e.g. `keyword` for foreground colors and `bgKeyword` for background colors).
- `chalk.bgHex('#DEADED').underline('Hello, world!')`
- `chalk.bgKeyword('orange')('Some orange text')`
- `chalk.bgRgb(15, 100, 204).inverse('Hello!')`
The following color models can be used:
- [`rgb`](https://en.wikipedia.org/wiki/RGB_color_model) - Example: `chalk.rgb(255, 136, 0).bold('Orange!')`
- [`hex`](https://en.wikipedia.org/wiki/Web_colors#Hex_triplet) - Example: `chalk.hex('#FF8800').bold('Orange!')`
- [`keyword`](https://www.w3.org/wiki/CSS/Properties/color/keywords) (CSS keywords) - Example: `chalk.keyword('orange').bold('Orange!')`
- [`hsl`](https://en.wikipedia.org/wiki/HSL_and_HSV) - Example: `chalk.hsl(32, 100, 50).bold('Orange!')`
- [`hsv`](https://en.wikipedia.org/wiki/HSL_and_HSV) - Example: `chalk.hsv(32, 100, 100).bold('Orange!')`
- [`hwb`](https://en.wikipedia.org/wiki/HWB_color_model) - Example: `chalk.hwb(32, 0, 50).bold('Orange!')`
- [`ansi`](https://en.wikipedia.org/wiki/ANSI_escape_code#3/4_bit) - Example: `chalk.ansi(31).bgAnsi(93)('red on yellowBright')`
- [`ansi256`](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit) - Example: `chalk.bgAnsi256(194)('Honeydew, more or less')`
## Windows
If you're on Windows, do yourself a favor and use [Windows Terminal](https://github.com/microsoft/terminal) instead of `cmd.exe`.
## Origin story
[colors.js](https://github.com/Marak/colors.js) used to be the most popular string styling module, but it has serious deficiencies like extending `String.prototype` which causes all kinds of [problems](https://github.com/yeoman/yo/issues/68) and the package is unmaintained. Although there are other packages, they either do too much or not enough. Chalk is a clean and focused alternative.
## chalk for enterprise
Available as part of the Tidelift Subscription.
The maintainers of chalk and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. [Learn more.](https://tidelift.com/subscription/pkg/npm-chalk?utm_source=npm-chalk&utm_medium=referral&utm_campaign=enterprise&utm_term=repo)
## Related
- [chalk-cli](https://github.com/chalk/chalk-cli) - CLI for this module
- [ansi-styles](https://github.com/chalk/ansi-styles) - ANSI escape codes for styling strings in the terminal
- [supports-color](https://github.com/chalk/supports-color) - Detect whether a terminal supports color
- [strip-ansi](https://github.com/chalk/strip-ansi) - Strip ANSI escape codes
- [strip-ansi-stream](https://github.com/chalk/strip-ansi-stream) - Strip ANSI escape codes from a stream
- [has-ansi](https://github.com/chalk/has-ansi) - Check if a string has ANSI escape codes
- [ansi-regex](https://github.com/chalk/ansi-regex) - Regular expression for matching ANSI escape codes
- [wrap-ansi](https://github.com/chalk/wrap-ansi) - Wordwrap a string with ANSI escape codes
- [slice-ansi](https://github.com/chalk/slice-ansi) - Slice a string with ANSI escape codes
- [color-convert](https://github.com/qix-/color-convert) - Converts colors between different models
- [chalk-animation](https://github.com/bokub/chalk-animation) - Animate strings in the terminal
- [gradient-string](https://github.com/bokub/gradient-string) - Apply color gradients to strings
- [chalk-pipe](https://github.com/LitoMore/chalk-pipe) - Create chalk style schemes with simpler style strings
- [terminal-link](https://github.com/sindresorhus/terminal-link) - Create clickable links in the terminal
## Maintainers
- [Sindre Sorhus](https://github.com/sindresorhus)
- [Josh Junon](https://github.com/qix-)

View File

@@ -0,0 +1,229 @@
'use strict';
const ansiStyles = require('ansi-styles');
const {stdout: stdoutColor, stderr: stderrColor} = require('supports-color');
const {
stringReplaceAll,
stringEncaseCRLFWithFirstIndex
} = require('./util');
const {isArray} = Array;
// `supportsColor.level` → `ansiStyles.color[name]` mapping
const levelMapping = [
'ansi',
'ansi',
'ansi256',
'ansi16m'
];
const styles = Object.create(null);
const applyOptions = (object, options = {}) => {
if (options.level && !(Number.isInteger(options.level) && options.level >= 0 && options.level <= 3)) {
throw new Error('The `level` option should be an integer from 0 to 3');
}
// Detect level if not set manually
const colorLevel = stdoutColor ? stdoutColor.level : 0;
object.level = options.level === undefined ? colorLevel : options.level;
};
class ChalkClass {
constructor(options) {
// eslint-disable-next-line no-constructor-return
return chalkFactory(options);
}
}
const chalkFactory = options => {
const chalk = {};
applyOptions(chalk, options);
chalk.template = (...arguments_) => chalkTag(chalk.template, ...arguments_);
Object.setPrototypeOf(chalk, Chalk.prototype);
Object.setPrototypeOf(chalk.template, chalk);
chalk.template.constructor = () => {
throw new Error('`chalk.constructor()` is deprecated. Use `new chalk.Instance()` instead.');
};
chalk.template.Instance = ChalkClass;
return chalk.template;
};
function Chalk(options) {
return chalkFactory(options);
}
for (const [styleName, style] of Object.entries(ansiStyles)) {
styles[styleName] = {
get() {
const builder = createBuilder(this, createStyler(style.open, style.close, this._styler), this._isEmpty);
Object.defineProperty(this, styleName, {value: builder});
return builder;
}
};
}
styles.visible = {
get() {
const builder = createBuilder(this, this._styler, true);
Object.defineProperty(this, 'visible', {value: builder});
return builder;
}
};
const usedModels = ['rgb', 'hex', 'keyword', 'hsl', 'hsv', 'hwb', 'ansi', 'ansi256'];
for (const model of usedModels) {
styles[model] = {
get() {
const {level} = this;
return function (...arguments_) {
const styler = createStyler(ansiStyles.color[levelMapping[level]][model](...arguments_), ansiStyles.color.close, this._styler);
return createBuilder(this, styler, this._isEmpty);
};
}
};
}
for (const model of usedModels) {
const bgModel = 'bg' + model[0].toUpperCase() + model.slice(1);
styles[bgModel] = {
get() {
const {level} = this;
return function (...arguments_) {
const styler = createStyler(ansiStyles.bgColor[levelMapping[level]][model](...arguments_), ansiStyles.bgColor.close, this._styler);
return createBuilder(this, styler, this._isEmpty);
};
}
};
}
const proto = Object.defineProperties(() => {}, {
...styles,
level: {
enumerable: true,
get() {
return this._generator.level;
},
set(level) {
this._generator.level = level;
}
}
});
const createStyler = (open, close, parent) => {
let openAll;
let closeAll;
if (parent === undefined) {
openAll = open;
closeAll = close;
} else {
openAll = parent.openAll + open;
closeAll = close + parent.closeAll;
}
return {
open,
close,
openAll,
closeAll,
parent
};
};
const createBuilder = (self, _styler, _isEmpty) => {
const builder = (...arguments_) => {
if (isArray(arguments_[0]) && isArray(arguments_[0].raw)) {
// Called as a template literal, for example: chalk.red`2 + 3 = {bold ${2+3}}`
return applyStyle(builder, chalkTag(builder, ...arguments_));
}
// Single argument is hot path, implicit coercion is faster than anything
// eslint-disable-next-line no-implicit-coercion
return applyStyle(builder, (arguments_.length === 1) ? ('' + arguments_[0]) : arguments_.join(' '));
};
// We alter the prototype because we must return a function, but there is
// no way to create a function with a different prototype
Object.setPrototypeOf(builder, proto);
builder._generator = self;
builder._styler = _styler;
builder._isEmpty = _isEmpty;
return builder;
};
const applyStyle = (self, string) => {
if (self.level <= 0 || !string) {
return self._isEmpty ? '' : string;
}
let styler = self._styler;
if (styler === undefined) {
return string;
}
const {openAll, closeAll} = styler;
if (string.indexOf('\u001B') !== -1) {
while (styler !== undefined) {
// Replace any instances already present with a re-opening code
// otherwise only the part of the string until said closing code
// will be colored, and the rest will simply be 'plain'.
string = stringReplaceAll(string, styler.close, styler.open);
styler = styler.parent;
}
}
// We can move both next actions out of loop, because remaining actions in loop won't have
// any/visible effect on parts we add here. Close the styling before a linebreak and reopen
// after next line to fix a bleed issue on macOS: https://github.com/chalk/chalk/pull/92
const lfIndex = string.indexOf('\n');
if (lfIndex !== -1) {
string = stringEncaseCRLFWithFirstIndex(string, closeAll, openAll, lfIndex);
}
return openAll + string + closeAll;
};
let template;
const chalkTag = (chalk, ...strings) => {
const [firstString] = strings;
if (!isArray(firstString) || !isArray(firstString.raw)) {
// If chalk() was called by itself or with a string,
// return the string itself as a string.
return strings.join(' ');
}
const arguments_ = strings.slice(1);
const parts = [firstString.raw[0]];
for (let i = 1; i < firstString.length; i++) {
parts.push(
String(arguments_[i - 1]).replace(/[{}\\]/g, '\\$&'),
String(firstString.raw[i])
);
}
if (template === undefined) {
template = require('./templates');
}
return template(chalk, parts.join(''));
};
Object.defineProperties(Chalk.prototype, styles);
const chalk = Chalk(); // eslint-disable-line new-cap
chalk.supportsColor = stdoutColor;
chalk.stderr = Chalk({level: stderrColor ? stderrColor.level : 0}); // eslint-disable-line new-cap
chalk.stderr.supportsColor = stderrColor;
module.exports = chalk;

View File

@@ -0,0 +1,134 @@
'use strict';
const TEMPLATE_REGEX = /(?:\\(u(?:[a-f\d]{4}|\{[a-f\d]{1,6}\})|x[a-f\d]{2}|.))|(?:\{(~)?(\w+(?:\([^)]*\))?(?:\.\w+(?:\([^)]*\))?)*)(?:[ \t]|(?=\r?\n)))|(\})|((?:.|[\r\n\f])+?)/gi;
const STYLE_REGEX = /(?:^|\.)(\w+)(?:\(([^)]*)\))?/g;
const STRING_REGEX = /^(['"])((?:\\.|(?!\1)[^\\])*)\1$/;
const ESCAPE_REGEX = /\\(u(?:[a-f\d]{4}|{[a-f\d]{1,6}})|x[a-f\d]{2}|.)|([^\\])/gi;
const ESCAPES = new Map([
['n', '\n'],
['r', '\r'],
['t', '\t'],
['b', '\b'],
['f', '\f'],
['v', '\v'],
['0', '\0'],
['\\', '\\'],
['e', '\u001B'],
['a', '\u0007']
]);
function unescape(c) {
const u = c[0] === 'u';
const bracket = c[1] === '{';
if ((u && !bracket && c.length === 5) || (c[0] === 'x' && c.length === 3)) {
return String.fromCharCode(parseInt(c.slice(1), 16));
}
if (u && bracket) {
return String.fromCodePoint(parseInt(c.slice(2, -1), 16));
}
return ESCAPES.get(c) || c;
}
function parseArguments(name, arguments_) {
const results = [];
const chunks = arguments_.trim().split(/\s*,\s*/g);
let matches;
for (const chunk of chunks) {
const number = Number(chunk);
if (!Number.isNaN(number)) {
results.push(number);
} else if ((matches = chunk.match(STRING_REGEX))) {
results.push(matches[2].replace(ESCAPE_REGEX, (m, escape, character) => escape ? unescape(escape) : character));
} else {
throw new Error(`Invalid Chalk template style argument: ${chunk} (in style '${name}')`);
}
}
return results;
}
function parseStyle(style) {
STYLE_REGEX.lastIndex = 0;
const results = [];
let matches;
while ((matches = STYLE_REGEX.exec(style)) !== null) {
const name = matches[1];
if (matches[2]) {
const args = parseArguments(name, matches[2]);
results.push([name].concat(args));
} else {
results.push([name]);
}
}
return results;
}
function buildStyle(chalk, styles) {
const enabled = {};
for (const layer of styles) {
for (const style of layer.styles) {
enabled[style[0]] = layer.inverse ? null : style.slice(1);
}
}
let current = chalk;
for (const [styleName, styles] of Object.entries(enabled)) {
if (!Array.isArray(styles)) {
continue;
}
if (!(styleName in current)) {
throw new Error(`Unknown Chalk style: ${styleName}`);
}
current = styles.length > 0 ? current[styleName](...styles) : current[styleName];
}
return current;
}
module.exports = (chalk, temporary) => {
const styles = [];
const chunks = [];
let chunk = [];
// eslint-disable-next-line max-params
temporary.replace(TEMPLATE_REGEX, (m, escapeCharacter, inverse, style, close, character) => {
if (escapeCharacter) {
chunk.push(unescape(escapeCharacter));
} else if (style) {
const string = chunk.join('');
chunk = [];
chunks.push(styles.length === 0 ? string : buildStyle(chalk, styles)(string));
styles.push({inverse, styles: parseStyle(style)});
} else if (close) {
if (styles.length === 0) {
throw new Error('Found extraneous } in Chalk template literal');
}
chunks.push(buildStyle(chalk, styles)(chunk.join('')));
chunk = [];
styles.pop();
} else {
chunk.push(character);
}
});
chunks.push(chunk.join(''));
if (styles.length > 0) {
const errMessage = `Chalk template literal is missing ${styles.length} closing bracket${styles.length === 1 ? '' : 's'} (\`}\`)`;
throw new Error(errMessage);
}
return chunks.join('');
};

View File

@@ -0,0 +1,39 @@
'use strict';
const stringReplaceAll = (string, substring, replacer) => {
let index = string.indexOf(substring);
if (index === -1) {
return string;
}
const substringLength = substring.length;
let endIndex = 0;
let returnValue = '';
do {
returnValue += string.substr(endIndex, index - endIndex) + substring + replacer;
endIndex = index + substringLength;
index = string.indexOf(substring, endIndex);
} while (index !== -1);
returnValue += string.substr(endIndex);
return returnValue;
};
const stringEncaseCRLFWithFirstIndex = (string, prefix, postfix, index) => {
let endIndex = 0;
let returnValue = '';
do {
const gotCR = string[index - 1] === '\r';
returnValue += string.substr(endIndex, (gotCR ? index - 1 : index) - endIndex) + prefix + (gotCR ? '\r\n' : '\n') + postfix;
endIndex = index + 1;
index = string.indexOf('\n', endIndex);
} while (index !== -1);
returnValue += string.substr(endIndex);
return returnValue;
};
module.exports = {
stringReplaceAll,
stringEncaseCRLFWithFirstIndex
};

View File

@@ -0,0 +1,39 @@
/**
Check if [`argv`](https://nodejs.org/docs/latest/api/process.html#process_process_argv) has a specific flag.
@param flag - CLI flag to look for. The `--` prefix is optional.
@param argv - CLI arguments. Default: `process.argv`.
@returns Whether the flag exists.
@example
```
// $ ts-node foo.ts -f --unicorn --foo=bar -- --rainbow
// foo.ts
import hasFlag = require('has-flag');
hasFlag('unicorn');
//=> true
hasFlag('--unicorn');
//=> true
hasFlag('f');
//=> true
hasFlag('-f');
//=> true
hasFlag('foo=bar');
//=> true
hasFlag('foo');
//=> false
hasFlag('rainbow');
//=> false
```
*/
declare function hasFlag(flag: string, argv?: string[]): boolean;
export = hasFlag;

View File

@@ -0,0 +1,8 @@
'use strict';
module.exports = (flag, argv = process.argv) => {
const prefix = flag.startsWith('-') ? '' : (flag.length === 1 ? '-' : '--');
const position = argv.indexOf(prefix + flag);
const terminatorPosition = argv.indexOf('--');
return position !== -1 && (terminatorPosition === -1 || position < terminatorPosition);
};

View File

@@ -0,0 +1,9 @@
MIT License
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,89 @@
# has-flag [![Build Status](https://travis-ci.org/sindresorhus/has-flag.svg?branch=master)](https://travis-ci.org/sindresorhus/has-flag)
> Check if [`argv`](https://nodejs.org/docs/latest/api/process.html#process_process_argv) has a specific flag
Correctly stops looking after an `--` argument terminator.
---
<div align="center">
<b>
<a href="https://tidelift.com/subscription/pkg/npm-has-flag?utm_source=npm-has-flag&utm_medium=referral&utm_campaign=readme">Get professional support for this package with a Tidelift subscription</a>
</b>
<br>
<sub>
Tidelift helps make open source sustainable for maintainers while giving companies<br>assurances about security, maintenance, and licensing for their dependencies.
</sub>
</div>
---
## Install
```
$ npm install has-flag
```
## Usage
```js
// foo.js
const hasFlag = require('has-flag');
hasFlag('unicorn');
//=> true
hasFlag('--unicorn');
//=> true
hasFlag('f');
//=> true
hasFlag('-f');
//=> true
hasFlag('foo=bar');
//=> true
hasFlag('foo');
//=> false
hasFlag('rainbow');
//=> false
```
```
$ node foo.js -f --unicorn --foo=bar -- --rainbow
```
## API
### hasFlag(flag, [argv])
Returns a boolean for whether the flag exists.
#### flag
Type: `string`
CLI flag to look for. The `--` prefix is optional.
#### argv
Type: `string[]`<br>
Default: `process.argv`
CLI arguments.
## Security
To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure.
## License
MIT © [Sindre Sorhus](https://sindresorhus.com)

View File

@@ -0,0 +1,24 @@
/* eslint-env browser */
'use strict';
function getChromeVersion() {
const matches = /(Chrome|Chromium)\/(?<chromeVersion>\d+)\./.exec(navigator.userAgent);
if (!matches) {
return;
}
return Number.parseInt(matches.groups.chromeVersion, 10);
}
const colorSupport = getChromeVersion() >= 69 ? {
level: 1,
hasBasic: true,
has256: false,
has16m: false
} : false;
module.exports = {
stdout: colorSupport,
stderr: colorSupport
};

View File

@@ -0,0 +1,152 @@
'use strict';
const os = require('os');
const tty = require('tty');
const hasFlag = require('has-flag');
const {env} = process;
let flagForceColor;
if (hasFlag('no-color') ||
hasFlag('no-colors') ||
hasFlag('color=false') ||
hasFlag('color=never')) {
flagForceColor = 0;
} else if (hasFlag('color') ||
hasFlag('colors') ||
hasFlag('color=true') ||
hasFlag('color=always')) {
flagForceColor = 1;
}
function envForceColor() {
if ('FORCE_COLOR' in env) {
if (env.FORCE_COLOR === 'true') {
return 1;
}
if (env.FORCE_COLOR === 'false') {
return 0;
}
return env.FORCE_COLOR.length === 0 ? 1 : Math.min(Number.parseInt(env.FORCE_COLOR, 10), 3);
}
}
function translateLevel(level) {
if (level === 0) {
return false;
}
return {
level,
hasBasic: true,
has256: level >= 2,
has16m: level >= 3
};
}
function supportsColor(haveStream, {streamIsTTY, sniffFlags = true} = {}) {
const noFlagForceColor = envForceColor();
if (noFlagForceColor !== undefined) {
flagForceColor = noFlagForceColor;
}
const forceColor = sniffFlags ? flagForceColor : noFlagForceColor;
if (forceColor === 0) {
return 0;
}
if (sniffFlags) {
if (hasFlag('color=16m') ||
hasFlag('color=full') ||
hasFlag('color=truecolor')) {
return 3;
}
if (hasFlag('color=256')) {
return 2;
}
}
if (haveStream && !streamIsTTY && forceColor === undefined) {
return 0;
}
const min = forceColor || 0;
if (env.TERM === 'dumb') {
return min;
}
if (process.platform === 'win32') {
// Windows 10 build 10586 is the first Windows release that supports 256 colors.
// Windows 10 build 14931 is the first release that supports 16m/TrueColor.
const osRelease = os.release().split('.');
if (
Number(osRelease[0]) >= 10 &&
Number(osRelease[2]) >= 10586
) {
return Number(osRelease[2]) >= 14931 ? 3 : 2;
}
return 1;
}
if ('CI' in env) {
if (['TRAVIS', 'CIRCLECI', 'APPVEYOR', 'GITLAB_CI', 'GITHUB_ACTIONS', 'BUILDKITE', 'DRONE'].some(sign => sign in env) || env.CI_NAME === 'codeship') {
return 1;
}
return min;
}
if ('TEAMCITY_VERSION' in env) {
return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(env.TEAMCITY_VERSION) ? 1 : 0;
}
if (env.COLORTERM === 'truecolor') {
return 3;
}
if ('TERM_PROGRAM' in env) {
const version = Number.parseInt((env.TERM_PROGRAM_VERSION || '').split('.')[0], 10);
switch (env.TERM_PROGRAM) {
case 'iTerm.app':
return version >= 3 ? 3 : 2;
case 'Apple_Terminal':
return 2;
// No default
}
}
if (/-256(color)?$/i.test(env.TERM)) {
return 2;
}
if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(env.TERM)) {
return 1;
}
if ('COLORTERM' in env) {
return 1;
}
return min;
}
function getSupportLevel(stream, options = {}) {
const level = supportsColor(stream, {
streamIsTTY: stream && stream.isTTY,
...options
});
return translateLevel(level);
}
module.exports = {
supportsColor: getSupportLevel,
stdout: getSupportLevel({isTTY: tty.isatty(1)}),
stderr: getSupportLevel({isTTY: tty.isatty(2)})
};

View File

@@ -0,0 +1,9 @@
MIT License
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,77 @@
# supports-color
> Detect whether a terminal supports color
## Install
```
$ npm install supports-color
```
## Usage
```js
const supportsColor = require('supports-color');
if (supportsColor.stdout) {
console.log('Terminal stdout supports color');
}
if (supportsColor.stdout.has256) {
console.log('Terminal stdout supports 256 colors');
}
if (supportsColor.stderr.has16m) {
console.log('Terminal stderr supports 16 million colors (truecolor)');
}
```
## API
Returns an `Object` with a `stdout` and `stderr` property for testing either streams. Each property is an `Object`, or `false` if color is not supported.
The `stdout`/`stderr` objects specifies a level of support for color through a `.level` property and a corresponding flag:
- `.level = 1` and `.hasBasic = true`: Basic color support (16 colors)
- `.level = 2` and `.has256 = true`: 256 color support
- `.level = 3` and `.has16m = true`: Truecolor support (16 million colors)
### `require('supports-color').supportsColor(stream, options?)`
Additionally, `supports-color` exposes the `.supportsColor()` function that takes an arbitrary write stream (e.g. `process.stdout`) and an optional options object to (re-)evaluate color support for an arbitrary stream.
For example, `require('supports-color').stdout` is the equivalent of `require('supports-color').supportsColor(process.stdout)`.
The options object supports a single boolean property `sniffFlags`. By default it is `true`, which instructs `supportsColor()` to sniff `process.argv` for the multitude of `--color` flags (see _Info_ below). If `false`, then `process.argv` is not considered when determining color support.
## Info
It obeys the `--color` and `--no-color` CLI flags.
For situations where using `--color` is not possible, use the environment variable `FORCE_COLOR=1` (level 1), `FORCE_COLOR=2` (level 2), or `FORCE_COLOR=3` (level 3) to forcefully enable color, or `FORCE_COLOR=0` to forcefully disable. The use of `FORCE_COLOR` overrides all other color support checks.
Explicit 256/Truecolor mode can be enabled using the `--color=256` and `--color=16m` flags, respectively.
## Related
- [supports-color-cli](https://github.com/chalk/supports-color-cli) - CLI for this module
- [chalk](https://github.com/chalk/chalk) - Terminal string styling done right
## Maintainers
- [Sindre Sorhus](https://github.com/sindresorhus)
- [Josh Junon](https://github.com/qix-)
---
<div align="center">
<b>
<a href="https://tidelift.com/subscription/pkg/npm-supports-color?utm_source=npm-supports-color&utm_medium=referral&utm_campaign=readme">Get professional support for this package with a Tidelift subscription</a>
</b>
<br>
<sub>
Tidelift helps make open source sustainable for maintainers while giving companies<br>assurances about security, maintenance, and licensing for their dependencies.
</sub>
</div>
---

View File

@@ -0,0 +1,13 @@
module.exports = class ExpandNpmShortcut {
parse(commandInfo) {
const [, npmCmd, cmdName, args] = commandInfo.command.match(/^(npm|yarn|pnpm):(\S+)(.*)/) || [];
if (!cmdName) {
return commandInfo;
}
return Object.assign({}, commandInfo, {
name: commandInfo.name || cmdName,
command: `${npmCmd} run ${cmdName}${args}`
});
}
};

View File

@@ -0,0 +1,36 @@
const ExpandNpmShortcut = require('./expand-npm-shortcut');
const parser = new ExpandNpmShortcut();
it('returns same command if no npm: prefix is present', () => {
const commandInfo = {
name: 'echo',
command: 'echo foo'
};
expect(parser.parse(commandInfo)).toBe(commandInfo);
});
for (const npmCmd of ['npm', 'yarn', 'pnpm']) {
describe(`with ${npmCmd}: prefix`, () => {
it(`expands to "${npmCmd} run <script> <args>"`, () => {
const commandInfo = {
name: 'echo',
command: `${npmCmd}:foo -- bar`
};
expect(parser.parse(commandInfo)).toEqual({
name: 'echo',
command: `${npmCmd} run foo -- bar`
});
});
it('sets name to script name if none', () => {
const commandInfo = {
command: `${npmCmd}:foo -- bar`
};
expect(parser.parse(commandInfo)).toEqual({
name: 'foo',
command: `${npmCmd} run foo -- bar`
});
});
});
}

View File

@@ -0,0 +1,43 @@
const _ = require('lodash');
const fs = require('fs');
module.exports = class ExpandNpmWildcard {
static readPackage() {
try {
const json = fs.readFileSync('package.json', { encoding: 'utf-8' });
return JSON.parse(json);
} catch (e) {
return {};
}
}
constructor(readPackage = ExpandNpmWildcard.readPackage) {
this.readPackage = readPackage;
}
parse(commandInfo) {
const [, npmCmd, cmdName, args] = commandInfo.command.match(/(npm|yarn|pnpm) run (\S+)([^&]*)/) || [];
const wildcardPosition = (cmdName || '').indexOf('*');
// If the regex didn't match an npm script, or it has no wildcard,
// then we have nothing to do here
if (!cmdName || wildcardPosition === -1) {
return commandInfo;
}
if (!this.scripts) {
this.scripts = Object.keys(this.readPackage().scripts || {});
}
const preWildcard = _.escapeRegExp(cmdName.substr(0, wildcardPosition));
const postWildcard = _.escapeRegExp(cmdName.substr(wildcardPosition + 1));
const wildcardRegex = new RegExp(`^${preWildcard}(.*?)${postWildcard}$`);
return this.scripts
.filter(script => wildcardRegex.test(script))
.map(script => Object.assign({}, commandInfo, {
command: `${npmCmd} run ${script}${args}`,
name: script
}));
}
};

View File

@@ -0,0 +1,58 @@
const ExpandNpmWildcard = require('./expand-npm-wildcard');
let parser, readPkg;
beforeEach(() => {
readPkg = jest.fn();
parser = new ExpandNpmWildcard(readPkg);
});
it('returns same command if not an npm run command', () => {
const commandInfo = {
command: 'npm test'
};
expect(readPkg).not.toHaveBeenCalled();
expect(parser.parse(commandInfo)).toBe(commandInfo);
});
it('returns same command if no wildcard present', () => {
const commandInfo = {
command: 'npm run foo bar'
};
expect(readPkg).not.toHaveBeenCalled();
expect(parser.parse(commandInfo)).toBe(commandInfo);
});
it('expands to nothing if no scripts exist in package.json', () => {
readPkg.mockReturnValue({});
expect(parser.parse({ command: 'npm run foo-*-baz qux' })).toEqual([]);
});
for (const npmCmd of ['npm', 'yarn', 'pnpm']) {
describe(`with an ${npmCmd}: prefix`, () => {
it('expands to all scripts matching pattern', () => {
readPkg.mockReturnValue({
scripts: {
'foo-bar-baz': '',
'foo--baz': '',
}
});
expect(parser.parse({ command: `${npmCmd} run foo-*-baz qux` })).toEqual([
{ name: 'foo-bar-baz', command: `${npmCmd} run foo-bar-baz qux` },
{ name: 'foo--baz', command: `${npmCmd} run foo--baz qux` },
]);
});
it('caches scripts upon calls', () => {
readPkg.mockReturnValue({});
parser.parse({ command: `${npmCmd} run foo-*-baz qux` });
parser.parse({ command: `${npmCmd} run foo-*-baz qux` });
expect(readPkg).toHaveBeenCalledTimes(1);
});
});
}

View File

@@ -0,0 +1,12 @@
module.exports = class StripQuotes {
parse(commandInfo) {
let { command } = commandInfo;
// Removes the quotes surrounding a command.
if (/^"(.+?)"$/.test(command) || /^'(.+?)'$/.test(command)) {
command = command.substr(1, command.length - 2);
}
return Object.assign({}, commandInfo, { command });
}
};

View File

@@ -0,0 +1,20 @@
const StripQuotes = require('./strip-quotes');
const parser = new StripQuotes();
it('returns command as is if no single/double quote at the beginning', () => {
expect(parser.parse({ command: 'echo foo' })).toEqual({ command: 'echo foo' });
});
it('strips single quotes', () => {
expect(parser.parse({ command: '\'echo foo\'' })).toEqual({ command: 'echo foo' });
});
it('strips double quotes', () => {
expect(parser.parse({ command: '"echo foo"' })).toEqual({ command: 'echo foo' });
});
it('does not remove quotes if they are impaired', () => {
expect(parser.parse({ command: '"echo foo' })).toEqual({ command: '"echo foo' });
expect(parser.parse({ command: 'echo foo\'' })).toEqual({ command: 'echo foo\'' });
expect(parser.parse({ command: '"echo foo\'' })).toEqual({ command: '"echo foo\'' });
});

View File

@@ -0,0 +1,63 @@
const Rx = require('rxjs');
module.exports = class Command {
get killable() {
return !!this.process;
}
constructor({ index, name, command, prefixColor, env, killProcess, spawn, spawnOpts }) {
this.index = index;
this.name = name;
this.command = command;
this.prefixColor = prefixColor;
this.env = env;
this.killProcess = killProcess;
this.spawn = spawn;
this.spawnOpts = spawnOpts;
this.killed = false;
this.error = new Rx.Subject();
this.close = new Rx.Subject();
this.stdout = new Rx.Subject();
this.stderr = new Rx.Subject();
}
start() {
const child = this.spawn(this.command, this.spawnOpts);
this.process = child;
this.pid = child.pid;
Rx.fromEvent(child, 'error').subscribe(event => {
this.process = undefined;
this.error.next(event);
});
Rx.fromEvent(child, 'close').subscribe(([exitCode, signal]) => {
this.process = undefined;
this.close.next({
command: {
command: this.command,
name: this.name,
prefixColor: this.prefixColor,
env: this.env,
},
index: this.index,
exitCode: exitCode === null ? signal : exitCode,
killed: this.killed,
});
});
child.stdout && pipeTo(Rx.fromEvent(child.stdout, 'data'), this.stdout);
child.stderr && pipeTo(Rx.fromEvent(child.stderr, 'data'), this.stderr);
this.stdin = child.stdin;
}
kill(code) {
if (this.killable) {
this.killed = true;
this.killProcess(this.pid, code);
}
}
};
function pipeTo(stream, subject) {
stream.subscribe(event => subject.next(event));
}

View File

@@ -0,0 +1,183 @@
const EventEmitter = require('events');
const Command = require('./command');
const createProcess = () => {
const process = new EventEmitter();
process.pid = 1;
return process;
};
const createProcessWithIO = () => {
const process = createProcess();
return Object.assign(process, {
stdout: new EventEmitter(),
stderr: new EventEmitter(),
stdin: new EventEmitter()
});
};
describe('#start()', () => {
it('spawns process with given command and options', () => {
const spawn = jest.fn().mockReturnValue(createProcess());
const command = new Command({
spawn,
spawnOpts: { bla: true },
command: 'echo foo',
});
command.start();
expect(spawn).toHaveBeenCalledTimes(1);
expect(spawn).toHaveBeenCalledWith(command.command, { bla: true });
});
it('sets stdin, process and PID', () => {
const process = createProcessWithIO();
const command = new Command({ spawn: () => process });
command.start();
expect(command.process).toBe(process);
expect(command.pid).toBe(process.pid);
expect(command.stdin).toBe(process.stdin);
});
it('shares errors to the error stream', done => {
const process = createProcess();
const command = new Command({ spawn: () => process });
command.error.subscribe(data => {
expect(data).toBe('foo');
expect(command.process).toBeUndefined();
done();
});
command.start();
process.emit('error', 'foo');
});
it('shares closes to the close stream with exit code', done => {
const process = createProcess();
const command = new Command({ spawn: () => process });
command.close.subscribe(data => {
expect(data.exitCode).toBe(0);
expect(data.killed).toBe(false);
expect(command.process).toBeUndefined();
done();
});
command.start();
process.emit('close', 0, null);
});
it('shares closes to the close stream with signal', done => {
const process = createProcess();
const command = new Command({ spawn: () => process });
command.close.subscribe(data => {
expect(data.exitCode).toBe('SIGKILL');
expect(data.killed).toBe(false);
done();
});
command.start();
process.emit('close', null, 'SIGKILL');
});
it('shares closes to the close stream with command info and index', done => {
const process = createProcess();
const commandInfo = {
command: 'cmd',
name: 'name',
prefixColor: 'green',
env: { VAR: 'yes' },
};
const command = new Command(
Object.assign({
index: 1,
spawn: () => process
}, commandInfo)
);
command.close.subscribe(data => {
expect(data.command).toEqual(commandInfo);
expect(data.killed).toBe(false);
expect(data.index).toBe(1);
done();
});
command.start();
process.emit('close', 0, null);
});
it('shares stdout to the stdout stream', done => {
const process = createProcessWithIO();
const command = new Command({ spawn: () => process });
command.stdout.subscribe(data => {
expect(data.toString()).toBe('hello');
done();
});
command.start();
process.stdout.emit('data', Buffer.from('hello'));
});
it('shares stderr to the stdout stream', done => {
const process = createProcessWithIO();
const command = new Command({ spawn: () => process });
command.stderr.subscribe(data => {
expect(data.toString()).toBe('dang');
done();
});
command.start();
process.stderr.emit('data', Buffer.from('dang'));
});
});
describe('#kill()', () => {
let process, killProcess, command;
beforeEach(() => {
process = createProcess();
killProcess = jest.fn();
command = new Command({ spawn: () => process, killProcess });
});
it('kills process', () => {
command.start();
command.kill();
expect(killProcess).toHaveBeenCalledTimes(1);
expect(killProcess).toHaveBeenCalledWith(command.pid, undefined);
});
it('kills process with some signal', () => {
command.start();
command.kill('SIGKILL');
expect(killProcess).toHaveBeenCalledTimes(1);
expect(killProcess).toHaveBeenCalledWith(command.pid, 'SIGKILL');
});
it('does not try to kill inexistent process', () => {
command.start();
process.emit('error');
command.kill();
expect(killProcess).not.toHaveBeenCalled();
});
it('marks the command as killed', done => {
command.start();
command.close.subscribe(data => {
expect(data.exitCode).toBe(1);
expect(data.killed).toBe(true);
done();
});
command.kill();
process.emit('close', 1, null);
});
});

View File

@@ -0,0 +1,39 @@
const Rx = require('rxjs');
const { bufferCount, switchMap, take } = require('rxjs/operators');
module.exports = class CompletionListener {
constructor({ successCondition, scheduler }) {
this.successCondition = successCondition;
this.scheduler = scheduler;
}
isSuccess(exitCodes) {
switch (this.successCondition) {
/* eslint-disable indent */
case 'first':
return exitCodes[0] === 0;
case 'last':
return exitCodes[exitCodes.length - 1] === 0;
default:
return exitCodes.every(exitCode => exitCode === 0);
/* eslint-enable indent */
}
}
listen(commands) {
const closeStreams = commands.map(command => command.close);
return Rx.merge(...closeStreams)
.pipe(
bufferCount(closeStreams.length),
switchMap(exitInfos =>
this.isSuccess(exitInfos.map(({ exitCode }) => exitCode))
? Rx.of(exitInfos, this.scheduler)
: Rx.throwError(exitInfos, this.scheduler)
),
take(1)
)
.toPromise();
}
};

View File

@@ -0,0 +1,88 @@
const { TestScheduler } = require('rxjs/testing');
const createFakeCommand = require('./flow-control/fixtures/fake-command');
const CompletionListener = require('./completion-listener');
let commands, scheduler;
beforeEach(() => {
commands = [createFakeCommand('foo'), createFakeCommand('bar')];
scheduler = new TestScheduler();
});
const createController = successCondition =>
new CompletionListener({
successCondition,
scheduler
});
describe('with default success condition set', () => {
it('succeeds if all processes exited with code 0', () => {
const result = createController().listen(commands);
commands[0].close.next({ exitCode: 0 });
commands[1].close.next({ exitCode: 0 });
scheduler.flush();
return expect(result).resolves.toEqual([{ exitCode: 0 }, { exitCode: 0 }]);
});
it('fails if one of the processes exited with non-0 code', () => {
const result = createController().listen(commands);
commands[0].close.next({ exitCode: 0 });
commands[1].close.next({ exitCode: 1 });
scheduler.flush();
expect(result).rejects.toEqual([{ exitCode: 0 }, { exitCode: 1 }]);
});
});
describe('with success condition set to first', () => {
it('succeeds if first process to exit has code 0', () => {
const result = createController('first').listen(commands);
commands[1].close.next({ exitCode: 0 });
commands[0].close.next({ exitCode: 1 });
scheduler.flush();
return expect(result).resolves.toEqual([{ exitCode: 0 }, { exitCode: 1 }]);
});
it('fails if first process to exit has non-0 code', () => {
const result = createController('first').listen(commands);
commands[1].close.next({ exitCode: 1 });
commands[0].close.next({ exitCode: 0 });
scheduler.flush();
return expect(result).rejects.toEqual([{ exitCode: 1 }, { exitCode: 0 }]);
});
});
describe('with success condition set to last', () => {
it('succeeds if last process to exit has code 0', () => {
const result = createController('last').listen(commands);
commands[1].close.next({ exitCode: 1 });
commands[0].close.next({ exitCode: 0 });
scheduler.flush();
return expect(result).resolves.toEqual([{ exitCode: 1 }, { exitCode: 0 }]);
});
it('fails if last process to exit has non-0 code', () => {
const result = createController('last').listen(commands);
commands[1].close.next({ exitCode: 0 });
commands[0].close.next({ exitCode: 1 });
scheduler.flush();
return expect(result).rejects.toEqual([{ exitCode: 0 }, { exitCode: 1 }]);
});
});

View File

@@ -0,0 +1,111 @@
const assert = require('assert');
const _ = require('lodash');
const spawn = require('spawn-command');
const treeKill = require('tree-kill');
const StripQuotes = require('./command-parser/strip-quotes');
const ExpandNpmShortcut = require('./command-parser/expand-npm-shortcut');
const ExpandNpmWildcard = require('./command-parser/expand-npm-wildcard');
const CompletionListener = require('./completion-listener');
const getSpawnOpts = require('./get-spawn-opts');
const Command = require('./command');
const defaults = {
spawn,
kill: treeKill,
raw: false,
controllers: [],
cwd: undefined,
};
module.exports = (commands, options) => {
assert.ok(Array.isArray(commands), '[concurrently] commands should be an array');
assert.notStrictEqual(commands.length, 0, '[concurrently] no commands provided');
options = _.defaults(options, defaults);
const commandParsers = [
new StripQuotes(),
new ExpandNpmShortcut(),
new ExpandNpmWildcard()
];
let lastColor = '';
commands = _(commands)
.map(mapToCommandInfo)
.flatMap(command => parseCommand(command, commandParsers))
.map((command, index) => {
// Use documented behaviour of repeating last color when specifying more commands than colors
lastColor = options.prefixColors && options.prefixColors[index] || lastColor;
return new Command(
Object.assign({
index,
spawnOpts: getSpawnOpts({
raw: options.raw,
env: command.env,
cwd: command.cwd || options.cwd,
}),
prefixColor: lastColor,
killProcess: options.kill,
spawn: options.spawn,
}, command)
);
})
.value();
const handleResult = options.controllers.reduce(
({ commands: prevCommands, onFinishCallbacks }, controller) => {
const { commands, onFinish } = controller.handle(prevCommands);
return {
commands,
onFinishCallbacks: _.concat(onFinishCallbacks, onFinish ? [onFinish] : [])
};
},
{ commands, onFinishCallbacks: [] }
);
commands = handleResult.commands;
const commandsLeft = commands.slice();
const maxProcesses = Math.max(1, Number(options.maxProcesses) || commandsLeft.length);
for (let i = 0; i < maxProcesses; i++) {
maybeRunMore(commandsLeft);
}
return new CompletionListener({ successCondition: options.successCondition })
.listen(commands)
.finally(() => {
handleResult.onFinishCallbacks.forEach((onFinish) => onFinish());
});
};
function mapToCommandInfo(command) {
return Object.assign({
command: command.command || command,
name: command.name || '',
env: command.env || {},
cwd: command.cwd || '',
}, command.prefixColor ? {
prefixColor: command.prefixColor,
} : {});
}
function parseCommand(command, parsers) {
return parsers.reduce(
(commands, parser) => _.flatMap(commands, command => parser.parse(command)),
_.castArray(command)
);
}
function maybeRunMore(commandsLeft) {
const command = commandsLeft.shift();
if (!command) {
return;
}
command.start();
command.close.subscribe(() => {
maybeRunMore(commandsLeft);
});
}

View File

@@ -0,0 +1,199 @@
const EventEmitter = require('events');
const createFakeCommand = require('./flow-control/fixtures/fake-command');
const FakeHandler = require('./flow-control/fixtures/fake-handler');
const concurrently = require('./concurrently');
let spawn, kill, controllers, processes = [];
const create = (commands, options = {}) => concurrently(
commands,
Object.assign(options, { controllers, spawn, kill })
);
beforeEach(() => {
processes = [];
spawn = jest.fn(() => {
const process = new EventEmitter();
processes.push(process);
process.pid = processes.length;
return process;
});
kill = jest.fn();
controllers = [new FakeHandler(), new FakeHandler()];
});
it('fails if commands is not an array', () => {
const bomb = () => create('foo');
expect(bomb).toThrowError();
});
it('fails if no commands were provided', () => {
const bomb = () => create([]);
expect(bomb).toThrowError();
});
it('spawns all commands', () => {
create(['echo', 'kill']);
expect(spawn).toHaveBeenCalledTimes(2);
expect(spawn).toHaveBeenCalledWith('echo', expect.objectContaining({}));
expect(spawn).toHaveBeenCalledWith('kill', expect.objectContaining({}));
});
it('spawns commands up to configured limit at once', () => {
create(['foo', 'bar', 'baz', 'qux'], { maxProcesses: 2 });
expect(spawn).toHaveBeenCalledTimes(2);
expect(spawn).toHaveBeenCalledWith('foo', expect.objectContaining({}));
expect(spawn).toHaveBeenCalledWith('bar', expect.objectContaining({}));
// Test out of order completion picking up new processes in-order
processes[1].emit('close', 1, null);
expect(spawn).toHaveBeenCalledTimes(3);
expect(spawn).toHaveBeenCalledWith('baz', expect.objectContaining({}));
processes[0].emit('close', null, 'SIGINT');
expect(spawn).toHaveBeenCalledTimes(4);
expect(spawn).toHaveBeenCalledWith('qux', expect.objectContaining({}));
// Shouldn't attempt to spawn anything else.
processes[2].emit('close', 1, null);
expect(spawn).toHaveBeenCalledTimes(4);
});
it('runs controllers with the commands', () => {
create(['echo', '"echo wrapped"']);
controllers.forEach(controller => {
expect(controller.handle).toHaveBeenCalledWith([
expect.objectContaining({ command: 'echo', index: 0 }),
expect.objectContaining({ command: 'echo wrapped', index: 1 }),
]);
});
});
it('runs commands with a name or prefix color', () => {
create([
{ command: 'echo', prefixColor: 'red', name: 'foo' },
'kill'
]);
controllers.forEach(controller => {
expect(controller.handle).toHaveBeenCalledWith([
expect.objectContaining({ command: 'echo', index: 0, name: 'foo', prefixColor: 'red' }),
expect.objectContaining({ command: 'kill', index: 1, name: '', prefixColor: '' }),
]);
});
});
it('runs commands with a list of colors', () => {
create(['echo', 'kill'], {
prefixColors: ['red']
});
controllers.forEach(controller => {
expect(controller.handle).toHaveBeenCalledWith([
expect.objectContaining({ command: 'echo', prefixColor: 'red' }),
expect.objectContaining({ command: 'kill', prefixColor: 'red' }),
]);
});
});
it('passes commands wrapped from a controller to the next one', () => {
const fakeCommand = createFakeCommand('banana', 'banana');
controllers[0].handle.mockReturnValue({ commands: [fakeCommand] });
create(['echo']);
expect(controllers[0].handle).toHaveBeenCalledWith([
expect.objectContaining({ command: 'echo', index: 0 })
]);
expect(controllers[1].handle).toHaveBeenCalledWith([fakeCommand]);
expect(fakeCommand.start).toHaveBeenCalledTimes(1);
});
it('merges extra env vars into each command', () => {
create([
{ command: 'echo', env: { foo: 'bar' } },
{ command: 'echo', env: { foo: 'baz' } },
'kill'
]);
expect(spawn).toHaveBeenCalledTimes(3);
expect(spawn).toHaveBeenCalledWith('echo', expect.objectContaining({
env: expect.objectContaining({ foo: 'bar' })
}));
expect(spawn).toHaveBeenCalledWith('echo', expect.objectContaining({
env: expect.objectContaining({ foo: 'baz' })
}));
expect(spawn).toHaveBeenCalledWith('kill', expect.objectContaining({
env: expect.not.objectContaining({ foo: expect.anything() })
}));
});
it('uses cwd from options for each command', () => {
create(
[
{ command: 'echo', env: { foo: 'bar' } },
{ command: 'echo', env: { foo: 'baz' } },
'kill'
],
{
cwd: 'foobar',
}
);
expect(spawn).toHaveBeenCalledTimes(3);
expect(spawn).toHaveBeenCalledWith('echo', expect.objectContaining({
env: expect.objectContaining({ foo: 'bar' }),
cwd: 'foobar',
}));
expect(spawn).toHaveBeenCalledWith('echo', expect.objectContaining({
env: expect.objectContaining({ foo: 'baz' }),
cwd: 'foobar',
}));
expect(spawn).toHaveBeenCalledWith('kill', expect.objectContaining({
env: expect.not.objectContaining({ foo: expect.anything() }),
cwd: 'foobar',
}));
});
it('uses overridden cwd option for each command if specified', () => {
create(
[
{ command: 'echo', env: { foo: 'bar' }, cwd: 'baz' },
{ command: 'echo', env: { foo: 'baz' } },
],
{
cwd: 'foobar',
}
);
expect(spawn).toHaveBeenCalledTimes(2);
expect(spawn).toHaveBeenCalledWith('echo', expect.objectContaining({
env: expect.objectContaining({ foo: 'bar' }),
cwd: 'baz',
}));
expect(spawn).toHaveBeenCalledWith('echo', expect.objectContaining({
env: expect.objectContaining({ foo: 'baz' }),
cwd: 'foobar',
}));
});
it('runs onFinish hook after all commands run', async () => {
const promise = create(['foo', 'bar'], { maxProcesses: 1 });
expect(spawn).toHaveBeenCalledTimes(1);
expect(controllers[0].onFinish).not.toHaveBeenCalled();
expect(controllers[1].onFinish).not.toHaveBeenCalled();
processes[0].emit('close', 0, null);
expect(spawn).toHaveBeenCalledTimes(2);
expect(controllers[0].onFinish).not.toHaveBeenCalled();
expect(controllers[1].onFinish).not.toHaveBeenCalled();
processes[1].emit('close', 0, null);
await promise;
expect(controllers[0].onFinish).toHaveBeenCalled();
expect(controllers[1].onFinish).toHaveBeenCalled();
});

View File

@@ -0,0 +1,33 @@
/**
* This file is meant to be a shared place for default configs.
* It's read by the flow controllers, the executable, etc.
*
* Refer to tests for the meaning of the different possible values.
*/
module.exports = {
defaultInputTarget: 0,
// Whether process.stdin should be forwarded to child processes
handleInput: false,
// How many processes to run at once
maxProcesses: 0,
// Indices and names of commands whose output to be not logged
hide: '',
nameSeparator: ',',
// Which prefix style to use when logging processes output.
prefix: '',
// Refer to https://www.npmjs.com/package/chalk
prefixColors: 'reset',
// How many bytes we'll show on the command prefix
prefixLength: 10,
raw: false,
// Number of attempts of restarting a process, if it exits with non-0 code
restartTries: 0,
// How many milliseconds concurrently should wait before restarting a process.
restartDelay: 0,
// Condition of success for concurrently itself.
success: 'all',
// Refer to https://date-fns.org/v2.0.1/docs/format
timestampFormat: 'yyyy-MM-dd HH:mm:ss.SSS',
// Current working dir passed as option to spawn command. Default: process.cwd()
cwd: undefined
};

View File

@@ -0,0 +1,16 @@
module.exports = class BaseHandler {
constructor(options = {}) {
const { logger } = options;
this.logger = logger;
}
handle(commands) {
return {
commands,
// an optional callback to call when all commands have finished
// (either successful or not)
onFinish: null,
};
}
};

View File

@@ -0,0 +1,50 @@
const Rx = require('rxjs');
const { map } = require('rxjs/operators');
const defaults = require('../defaults');
const BaseHandler = require('./base-handler');
module.exports = class InputHandler extends BaseHandler {
constructor({ defaultInputTarget, inputStream, pauseInputStreamOnFinish, logger }) {
super({ logger });
this.defaultInputTarget = defaultInputTarget || defaults.defaultInputTarget;
this.inputStream = inputStream;
this.pauseInputStreamOnFinish = pauseInputStreamOnFinish !== false;
}
handle(commands) {
if (!this.inputStream) {
return { commands };
}
Rx.fromEvent(this.inputStream, 'data')
.pipe(map(data => data.toString()))
.subscribe(data => {
let [targetId, input] = data.split(/:(.+)/);
targetId = input ? targetId : this.defaultInputTarget;
input = input || data;
const command = commands.find(command => (
command.name === targetId ||
command.index.toString() === targetId.toString()
));
if (command && command.stdin) {
command.stdin.write(input);
} else {
this.logger.logGlobalEvent(`Unable to find command ${targetId}, or it has no stdin open\n`);
}
});
return {
commands,
onFinish: () => {
if (this.pauseInputStreamOnFinish) {
// https://github.com/kimmobrunfeldt/concurrently/issues/252
this.inputStream.pause();
}
},
};
}
};

View File

@@ -0,0 +1,113 @@
const stream = require('stream');
const { createMockInstance } = require('jest-create-mock-instance');
const Logger = require('../logger');
const createFakeCommand = require('./fixtures/fake-command');
const InputHandler = require('./input-handler');
let commands, controller, inputStream, logger;
beforeEach(() => {
commands = [
createFakeCommand('foo', 'echo foo', 0),
createFakeCommand('bar', 'echo bar', 1),
];
inputStream = new stream.PassThrough();
logger = createMockInstance(Logger);
controller = new InputHandler({
defaultInputTarget: 0,
inputStream,
logger
});
});
it('returns same commands', () => {
expect(controller.handle(commands)).toMatchObject({ commands });
controller = new InputHandler({ logger });
expect(controller.handle(commands)).toMatchObject({ commands });
});
it('forwards input stream to default target ID', () => {
controller.handle(commands);
inputStream.write('something');
expect(commands[0].stdin.write).toHaveBeenCalledTimes(1);
expect(commands[0].stdin.write).toHaveBeenCalledWith('something');
expect(commands[1].stdin.write).not.toHaveBeenCalled();
});
it('forwards input stream to target index specified in input', () => {
controller.handle(commands);
inputStream.write('1:something');
expect(commands[0].stdin.write).not.toHaveBeenCalled();
expect(commands[1].stdin.write).toHaveBeenCalledTimes(1);
expect(commands[1].stdin.write).toHaveBeenCalledWith('something');
});
it('forwards input stream to target index specified in input when input contains colon', () => {
controller.handle(commands);
inputStream.emit('data', Buffer.from('1::something'));
inputStream.emit('data', Buffer.from('1:some:thing'));
expect(commands[0].stdin.write).not.toHaveBeenCalled();
expect(commands[1].stdin.write).toHaveBeenCalledTimes(2);
expect(commands[1].stdin.write).toHaveBeenCalledWith(':something');
expect(commands[1].stdin.write).toHaveBeenCalledWith('some:thing');
});
it('forwards input stream to target name specified in input', () => {
controller.handle(commands);
inputStream.write('bar:something');
expect(commands[0].stdin.write).not.toHaveBeenCalled();
expect(commands[1].stdin.write).toHaveBeenCalledTimes(1);
expect(commands[1].stdin.write).toHaveBeenCalledWith('something');
});
it('logs error if command has no stdin open', () => {
commands[0].stdin = null;
controller.handle(commands);
inputStream.write('something');
expect(commands[1].stdin.write).not.toHaveBeenCalled();
expect(logger.logGlobalEvent).toHaveBeenCalledWith('Unable to find command 0, or it has no stdin open\n');
});
it('logs error if command is not found', () => {
controller.handle(commands);
inputStream.write('foobar:something');
expect(commands[0].stdin.write).not.toHaveBeenCalled();
expect(commands[1].stdin.write).not.toHaveBeenCalled();
expect(logger.logGlobalEvent).toHaveBeenCalledWith('Unable to find command foobar, or it has no stdin open\n');
});
it('pauses input stream when finished', () => {
expect(inputStream.readableFlowing).toBeNull();
const { onFinish } = controller.handle(commands);
expect(inputStream.readableFlowing).toBe(true);
onFinish();
expect(inputStream.readableFlowing).toBe(false);
});
it('does not pause input stream when pauseInputStreamOnFinish is set to false', () => {
controller = new InputHandler({ inputStream, pauseInputStreamOnFinish: false });
expect(inputStream.readableFlowing).toBeNull();
const { onFinish } = controller.handle(commands);
expect(inputStream.readableFlowing).toBe(true);
onFinish();
expect(inputStream.readableFlowing).toBe(true);
});

View File

@@ -0,0 +1,35 @@
const { map } = require('rxjs/operators');
const BaseHandler = require('./base-handler');
module.exports = class KillOnSignal extends BaseHandler {
constructor({ process }) {
super();
this.process = process;
}
handle(commands) {
let caughtSignal;
['SIGINT', 'SIGTERM', 'SIGHUP'].forEach(signal => {
this.process.on(signal, () => {
caughtSignal = signal;
commands.forEach(command => command.kill(signal));
});
});
return {
commands: commands.map(command => {
const closeStream = command.close.pipe(map(exitInfo => {
const exitCode = caughtSignal === 'SIGINT' ? 0 : exitInfo.exitCode;
return Object.assign({}, exitInfo, { exitCode });
}));
return new Proxy(command, {
get(target, prop) {
return prop === 'close' ? closeStream : target[prop];
}
});
})
};
}
};

View File

@@ -0,0 +1,79 @@
const EventEmitter = require('events');
const createFakeCommand = require('./fixtures/fake-command');
const KillOnSignal = require('./kill-on-signal');
let commands, controller, process;
beforeEach(() => {
process = new EventEmitter();
commands = [
createFakeCommand(),
createFakeCommand(),
];
controller = new KillOnSignal({ process });
});
it('returns commands that keep non-close streams from original commands', () => {
const { commands: newCommands } = controller.handle(commands);
newCommands.forEach((newCommand, i) => {
expect(newCommand.close).not.toBe(commands[i].close);
expect(newCommand.error).toBe(commands[i].error);
expect(newCommand.stdout).toBe(commands[i].stdout);
expect(newCommand.stderr).toBe(commands[i].stderr);
});
});
it('returns commands that map SIGINT to exit code 0', () => {
const { commands: newCommands } = controller.handle(commands);
expect(newCommands).not.toBe(commands);
expect(newCommands).toHaveLength(commands.length);
const callback = jest.fn();
newCommands[0].close.subscribe(callback);
process.emit('SIGINT');
// A fake command's .kill() call won't trigger a close event automatically...
commands[0].close.next({ exitCode: 1 });
expect(callback).not.toHaveBeenCalledWith({ exitCode: 'SIGINT' });
expect(callback).toHaveBeenCalledWith({ exitCode: 0 });
});
it('returns commands that keep non-SIGINT exit codes', () => {
const { commands: newCommands } = controller.handle(commands);
expect(newCommands).not.toBe(commands);
expect(newCommands).toHaveLength(commands.length);
const callback = jest.fn();
newCommands[0].close.subscribe(callback);
commands[0].close.next({ exitCode: 1 });
expect(callback).toHaveBeenCalledWith({ exitCode: 1 });
});
it('kills all commands on SIGINT', () => {
controller.handle(commands);
process.emit('SIGINT');
expect(process.listenerCount('SIGINT')).toBe(1);
expect(commands[0].kill).toHaveBeenCalledWith('SIGINT');
expect(commands[1].kill).toHaveBeenCalledWith('SIGINT');
});
it('kills all commands on SIGTERM', () => {
controller.handle(commands);
process.emit('SIGTERM');
expect(process.listenerCount('SIGTERM')).toBe(1);
expect(commands[0].kill).toHaveBeenCalledWith('SIGTERM');
expect(commands[1].kill).toHaveBeenCalledWith('SIGTERM');
});
it('kills all commands on SIGHUP', () => {
controller.handle(commands);
process.emit('SIGHUP');
expect(process.listenerCount('SIGHUP')).toBe(1);
expect(commands[0].kill).toHaveBeenCalledWith('SIGHUP');
expect(commands[1].kill).toHaveBeenCalledWith('SIGHUP');
});

View File

@@ -0,0 +1,38 @@
const _ = require('lodash');
const { filter, map } = require('rxjs/operators');
const BaseHandler = require('./base-handler');
module.exports = class KillOthers extends BaseHandler {
constructor({ logger, conditions }) {
super({ logger });
this.conditions = _.castArray(conditions);
}
handle(commands) {
const conditions = this.conditions.filter(condition => (
condition === 'failure' ||
condition === 'success'
));
if (!conditions.length) {
return { commands };
}
const closeStates = commands.map(command => command.close.pipe(
map(({ exitCode }) => exitCode === 0 ? 'success' : 'failure'),
filter(state => conditions.includes(state))
));
closeStates.forEach(closeState => closeState.subscribe(() => {
const killableCommands = commands.filter(command => command.killable);
if (killableCommands.length) {
this.logger.logGlobalEvent('Sending SIGTERM to other processes..');
killableCommands.forEach(command => command.kill());
}
}));
return { commands };
}
};

View File

@@ -0,0 +1,66 @@
const { createMockInstance } = require('jest-create-mock-instance');
const Logger = require('../logger');
const createFakeCommand = require('./fixtures/fake-command');
const KillOthers = require('./kill-others');
let commands, logger;
beforeEach(() => {
commands = [
createFakeCommand(),
createFakeCommand()
];
logger = createMockInstance(Logger);
});
const createWithConditions = conditions => new KillOthers({
logger,
conditions
});
it('returns same commands', () => {
expect(createWithConditions(['foo']).handle(commands)).toMatchObject({ commands });
expect(createWithConditions(['failure']).handle(commands)).toMatchObject({ commands });
});
it('does not kill others if condition does not match', () => {
createWithConditions(['failure']).handle(commands);
commands[1].killable = true;
commands[0].close.next({ exitCode: 0 });
expect(logger.logGlobalEvent).not.toHaveBeenCalled();
expect(commands[0].kill).not.toHaveBeenCalled();
expect(commands[1].kill).not.toHaveBeenCalled();
});
it('kills other killable processes on success', () => {
createWithConditions(['success']).handle(commands);
commands[1].killable = true;
commands[0].close.next({ exitCode: 0 });
expect(logger.logGlobalEvent).toHaveBeenCalledTimes(1);
expect(logger.logGlobalEvent).toHaveBeenCalledWith('Sending SIGTERM to other processes..');
expect(commands[0].kill).not.toHaveBeenCalled();
expect(commands[1].kill).toHaveBeenCalled();
});
it('kills other killable processes on failure', () => {
createWithConditions(['failure']).handle(commands);
commands[1].killable = true;
commands[0].close.next({ exitCode: 1 });
expect(logger.logGlobalEvent).toHaveBeenCalledTimes(1);
expect(logger.logGlobalEvent).toHaveBeenCalledWith('Sending SIGTERM to other processes..');
expect(commands[0].kill).not.toHaveBeenCalled();
expect(commands[1].kill).toHaveBeenCalled();
});
it('does not try to kill processes already dead', () => {
createWithConditions(['failure']).handle(commands);
commands[0].close.next({ exitCode: 1 });
expect(logger.logGlobalEvent).not.toHaveBeenCalled();
expect(commands[0].kill).not.toHaveBeenCalled();
expect(commands[1].kill).not.toHaveBeenCalled();
});

View File

@@ -0,0 +1,18 @@
const { of } = require('rxjs');
const BaseHandler = require('./base-handler');
module.exports = class LogExit extends BaseHandler {
handle(commands) {
commands.forEach(command => command.error.subscribe(event => {
this.logger.logCommandEvent(
`Error occurred when executing command: ${command.command}`,
command
);
this.logger.logCommandEvent(event.stack || event, command);
}));
return { commands };
}
};

View File

@@ -0,0 +1,40 @@
const { createMockInstance } = require('jest-create-mock-instance');
const Logger = require('../logger');
const LogError = require('./log-error');
const createFakeCommand = require('./fixtures/fake-command');
let controller, logger, commands;
beforeEach(() => {
commands = [
createFakeCommand(),
createFakeCommand(),
];
logger = createMockInstance(Logger);
controller = new LogError({ logger });
});
it('returns same commands', () => {
expect(controller.handle(commands)).toMatchObject({ commands });
});
it('logs the error event of each command', () => {
controller.handle(commands);
commands[0].error.next('error from command 0');
const error = new Error('some error message');
commands[1].error.next(error);
expect(logger.logCommandEvent).toHaveBeenCalledTimes(4);
expect(logger.logCommandEvent).toHaveBeenCalledWith(
`Error occurred when executing command: ${commands[0].command}`,
commands[0]
);
expect(logger.logCommandEvent).toHaveBeenCalledWith('error from command 0', commands[0]);
expect(logger.logCommandEvent).toHaveBeenCalledWith(
`Error occurred when executing command: ${commands[1].command}`,
commands[1]
);
expect(logger.logCommandEvent).toHaveBeenCalledWith(error.stack, commands[1]);
});

View File

@@ -0,0 +1,11 @@
const BaseHandler = require('./base-handler');
module.exports = class LogExit extends BaseHandler {
handle(commands) {
commands.forEach(command => command.close.subscribe(({ exitCode }) => {
this.logger.logCommandEvent(`${command.command} exited with code ${exitCode}`, command);
}));
return { commands };
}
};

View File

@@ -0,0 +1,36 @@
const { createMockInstance } = require('jest-create-mock-instance');
const Logger = require('../logger');
const LogExit = require('./log-exit');
const createFakeCommand = require('./fixtures/fake-command');
let controller, logger, commands;
beforeEach(() => {
commands = [
createFakeCommand(),
createFakeCommand(),
];
logger = createMockInstance(Logger);
controller = new LogExit({ logger });
});
it('returns same commands', () => {
expect(controller.handle(commands)).toMatchObject({ commands });
});
it('logs the close event of each command', () => {
controller.handle(commands);
commands[0].close.next({ exitCode: 0 });
commands[1].close.next({ exitCode: 'SIGTERM' });
expect(logger.logCommandEvent).toHaveBeenCalledTimes(2);
expect(logger.logCommandEvent).toHaveBeenCalledWith(
`${commands[0].command} exited with code 0`,
commands[0]
);
expect(logger.logCommandEvent).toHaveBeenCalledWith(
`${commands[1].command} exited with code SIGTERM`,
commands[1]
);
});

View File

@@ -0,0 +1,12 @@
const BaseHandler = require('./base-handler');
module.exports = class LogOutput extends BaseHandler {
handle(commands) {
commands.forEach(command => {
command.stdout.subscribe(text => this.logger.logCommandText(text.toString(), command));
command.stderr.subscribe(text => this.logger.logCommandText(text.toString(), command));
});
return { commands };
}
};

View File

@@ -0,0 +1,41 @@
const { createMockInstance } = require('jest-create-mock-instance');
const Logger = require('../logger');
const LogOutput = require('./log-output');
const createFakeCommand = require('./fixtures/fake-command');
let controller, logger, commands;
beforeEach(() => {
commands = [
createFakeCommand(),
createFakeCommand(),
];
logger = createMockInstance(Logger);
controller = new LogOutput({ logger });
});
it('returns same commands', () => {
expect(controller.handle(commands)).toMatchObject({ commands });
});
it('logs the stdout of each command', () => {
controller.handle(commands);
commands[0].stdout.next(Buffer.from('foo'));
commands[1].stdout.next(Buffer.from('bar'));
expect(logger.logCommandText).toHaveBeenCalledTimes(2);
expect(logger.logCommandText).toHaveBeenCalledWith('foo', commands[0]);
expect(logger.logCommandText).toHaveBeenCalledWith('bar', commands[1]);
});
it('logs the stderr of each command', () => {
controller.handle(commands);
commands[0].stderr.next(Buffer.from('foo'));
commands[1].stderr.next(Buffer.from('bar'));
expect(logger.logCommandText).toHaveBeenCalledTimes(2);
expect(logger.logCommandText).toHaveBeenCalledWith('foo', commands[0]);
expect(logger.logCommandText).toHaveBeenCalledWith('bar', commands[1]);
});

View File

@@ -0,0 +1,56 @@
const Rx = require('rxjs');
const { defaultIfEmpty, delay, filter, mapTo, skip, take, takeWhile } = require('rxjs/operators');
const defaults = require('../defaults');
const BaseHandler = require('./base-handler');
module.exports = class RestartProcess extends BaseHandler {
constructor({ delay, tries, logger, scheduler }) {
super({ logger });
this.delay = +delay || defaults.restartDelay;
this.tries = +tries || defaults.restartTries;
this.tries = this.tries < 0 ? Infinity : this.tries;
this.scheduler = scheduler;
}
handle(commands) {
if (this.tries === 0) {
return { commands };
}
commands.map(command => command.close.pipe(
take(this.tries),
takeWhile(({ exitCode }) => exitCode !== 0)
)).map((failure, index) => Rx.merge(
// Delay the emission (so that the restarts happen on time),
// explicitly telling the subscriber that a restart is needed
failure.pipe(delay(this.delay, this.scheduler), mapTo(true)),
// Skip the first N emissions (as these would be duplicates of the above),
// meaning it will be empty because of success, or failed all N times,
// and no more restarts should be attempted.
failure.pipe(skip(this.tries), defaultIfEmpty(false))
).subscribe(restart => {
const command = commands[index];
if (restart) {
this.logger.logCommandEvent(`${command.command} restarted`, command);
command.start();
}
}));
return {
commands: commands.map(command => {
const closeStream = command.close.pipe(filter(({ exitCode }, emission) => {
// We let all success codes pass, and failures only after restarting won't happen again
return exitCode === 0 || emission >= this.tries;
}));
return new Proxy(command, {
get(target, prop) {
return prop === 'close' ? closeStream : target[prop];
}
});
})
};
}
};

View File

@@ -0,0 +1,131 @@
const { createMockInstance } = require('jest-create-mock-instance');
const { TestScheduler } = require('rxjs/testing');
const Logger = require('../logger');
const createFakeCommand = require('./fixtures/fake-command');
const RestartProcess = require('./restart-process');
let commands, controller, logger, scheduler;
beforeEach(() => {
commands = [
createFakeCommand(),
createFakeCommand()
];
logger = createMockInstance(Logger);
scheduler = new TestScheduler();
controller = new RestartProcess({
logger,
scheduler,
delay: 100,
tries: 2
});
});
it('does not restart processes that complete with success', () => {
controller.handle(commands);
commands[0].close.next({ exitCode: 0 });
commands[1].close.next({ exitCode: 0 });
scheduler.flush();
expect(commands[0].start).toHaveBeenCalledTimes(0);
expect(commands[1].start).toHaveBeenCalledTimes(0);
});
it('restarts processes that fail after delay has passed', () => {
controller.handle(commands);
commands[0].close.next({ exitCode: 1 });
commands[1].close.next({ exitCode: 0 });
scheduler.flush();
expect(logger.logCommandEvent).toHaveBeenCalledTimes(1);
expect(logger.logCommandEvent).toHaveBeenCalledWith(
`${commands[0].command} restarted`,
commands[0]
);
expect(commands[0].start).toHaveBeenCalledTimes(1);
expect(commands[1].start).not.toHaveBeenCalled();
});
it('restarts processes up to tries', () => {
controller.handle(commands);
commands[0].close.next({ exitCode: 1 });
commands[0].close.next({ exitCode: 'SIGTERM' });
commands[0].close.next({ exitCode: 'SIGTERM' });
commands[1].close.next({ exitCode: 0 });
scheduler.flush();
expect(logger.logCommandEvent).toHaveBeenCalledTimes(2);
expect(logger.logCommandEvent).toHaveBeenCalledWith(
`${commands[0].command} restarted`,
commands[0]
);
expect(commands[0].start).toHaveBeenCalledTimes(2);
});
it.todo('restart processes forever, if tries is negative');
it('restarts processes until they succeed', () => {
controller.handle(commands);
commands[0].close.next({ exitCode: 1 });
commands[0].close.next({ exitCode: 0 });
commands[1].close.next({ exitCode: 0 });
scheduler.flush();
expect(logger.logCommandEvent).toHaveBeenCalledTimes(1);
expect(logger.logCommandEvent).toHaveBeenCalledWith(
`${commands[0].command} restarted`,
commands[0]
);
expect(commands[0].start).toHaveBeenCalledTimes(1);
});
describe('returned commands', () => {
it('are the same if 0 tries are to be attempted', () => {
controller = new RestartProcess({ logger, scheduler });
expect(controller.handle(commands)).toMatchObject({ commands });
});
it('are not the same, but with same length if 1+ tries are to be attempted', () => {
const { commands: newCommands } = controller.handle(commands);
expect(newCommands).not.toBe(commands);
expect(newCommands).toHaveLength(commands.length);
});
it('skip close events followed by restarts', () => {
const { commands: newCommands } = controller.handle(commands);
const callback = jest.fn();
newCommands[0].close.subscribe(callback);
newCommands[1].close.subscribe(callback);
commands[0].close.next({ exitCode: 1 });
commands[0].close.next({ exitCode: 1 });
commands[0].close.next({ exitCode: 1 });
commands[1].close.next({ exitCode: 1 });
commands[1].close.next({ exitCode: 0 });
scheduler.flush();
// 1 failure from commands[0], 1 success from commands[1]
expect(callback).toHaveBeenCalledTimes(2);
});
it('keep non-close streams from original commands', () => {
const { commands: newCommands } = controller.handle(commands);
newCommands.forEach((newCommand, i) => {
expect(newCommand.close).not.toBe(commands[i].close);
expect(newCommand.error).toBe(commands[i].error);
expect(newCommand.stdout).toBe(commands[i].stdout);
expect(newCommand.stderr).toBe(commands[i].stderr);
});
});
});

View File

@@ -0,0 +1,16 @@
const supportsColor = require('supports-color');
module.exports = ({
colorSupport = supportsColor.stdout,
cwd,
process = global.process,
raw = false,
env = {},
}) => Object.assign(
{
cwd: cwd || process.cwd(),
},
raw && { stdio: 'inherit' },
/^win/.test(process.platform) && { detached: false },
{ env: Object.assign(colorSupport ? { FORCE_COLOR: colorSupport.level } : {}, process.env, env) }
);

View File

@@ -0,0 +1,30 @@
const getSpawnOpts = require('./get-spawn-opts');
it('sets detached mode to false for Windows platform', () => {
expect(getSpawnOpts({ process: { platform: 'win32', cwd: jest.fn() } }).detached).toBe(false);
});
it('sets stdio to inherit when raw', () => {
expect(getSpawnOpts({ raw: true }).stdio).toBe('inherit');
});
it('merges FORCE_COLOR into env vars if color supported', () => {
const process = { env: { foo: 'bar' }, cwd: jest.fn() };
expect(getSpawnOpts({ process, colorSupport: false }).env).toEqual(process.env);
expect(getSpawnOpts({ process, colorSupport: { level: 1 } }).env).toEqual({
FORCE_COLOR: 1,
foo: 'bar'
});
});
it('sets default cwd to process.cwd()', () => {
const process = { cwd: jest.fn().mockReturnValue('process-cwd') };
expect(getSpawnOpts({
process,
}).cwd).toBe('process-cwd');
});
it('overrides default cwd', () => {
const cwd = 'foobar';
expect(getSpawnOpts({ cwd }).cwd).toBe(cwd);
});

View File

@@ -0,0 +1,123 @@
const chalk = require('chalk');
const _ = require('lodash');
const formatDate = require('date-fns/format');
const defaults = require('./defaults');
module.exports = class Logger {
constructor({ hide, outputStream, prefixFormat, prefixLength, raw, timestampFormat }) {
// To avoid empty strings from hiding the output of commands that don't have a name,
// keep in the list of commands to hide only strings with some length.
// This might happen through the CLI when no `--hide` argument is specified, for example.
this.hide = _.castArray(hide).filter(name => name || name === 0).map(String);
this.raw = raw;
this.outputStream = outputStream;
this.prefixFormat = prefixFormat;
this.prefixLength = prefixLength || defaults.prefixLength;
this.timestampFormat = timestampFormat || defaults.timestampFormat;
}
shortenText(text) {
if (!text || text.length <= this.prefixLength) {
return text;
}
const ellipsis = '..';
const prefixLength = this.prefixLength - ellipsis.length;
const endLength = Math.floor(prefixLength / 2);
const beginningLength = prefixLength - endLength;
const beginnning = text.substring(0, beginningLength);
const end = text.substring(text.length - endLength, text.length);
return beginnning + ellipsis + end;
}
getPrefixesFor(command) {
return {
none: '',
pid: command.pid,
index: command.index,
name: command.name,
command: this.shortenText(command.command),
time: formatDate(Date.now(), this.timestampFormat)
};
}
getPrefix(command) {
const prefix = this.prefixFormat || (command.name ? 'name' : 'index');
if (prefix === 'none') {
return '';
}
const prefixes = this.getPrefixesFor(command);
if (Object.keys(prefixes).includes(prefix)) {
return `[${prefixes[prefix]}]`;
}
return _.reduce(prefixes, (prev, val, key) => {
const keyRegex = new RegExp(_.escapeRegExp(`{${key}}`), 'g');
return prev.replace(keyRegex, val);
}, prefix);
}
colorText(command, text) {
let color;
if (command.prefixColor && command.prefixColor.startsWith('#')) {
color = chalk.hex(command.prefixColor);
} else {
const defaultColor = _.get(chalk, defaults.prefixColors, chalk.reset);
color = _.get(chalk, command.prefixColor, defaultColor);
}
return color(text);
}
logCommandEvent(text, command) {
if (this.raw) {
return;
}
this.logCommandText(chalk.reset(text) + '\n', command);
}
logCommandText(text, command) {
if (this.hide.includes(String(command.index)) || this.hide.includes(command.name)) {
return;
}
const prefix = this.colorText(command, this.getPrefix(command));
return this.log(prefix + (prefix ? ' ' : ''), text);
}
logGlobalEvent(text) {
if (this.raw) {
return;
}
this.log(chalk.reset('-->') + ' ', chalk.reset(text) + '\n');
}
log(prefix, text) {
if (this.raw) {
return this.outputStream.write(text);
}
// #70 - replace some ANSI code that would impact clearing lines
text = text.replace(/\u2026/g, '...');
const lines = text.split('\n').map((line, index, lines) => {
// First line will write prefix only if we finished the last write with a LF.
// Last line won't write prefix because it should be empty.
if (index === 0 || index === lines.length - 1) {
return line;
}
return prefix + line;
});
if (!this.lastChar || this.lastChar === '\n') {
this.outputStream.write(prefix);
}
this.lastChar = text[text.length - 1];
this.outputStream.write(lines.join('\n'));
}
};

View File

@@ -0,0 +1,223 @@
const { Writable } = require('stream');
const chalk = require('chalk');
const { createMockInstance } = require('jest-create-mock-instance');
const Logger = require('./logger');
let outputStream;
beforeEach(() => {
outputStream = createMockInstance(Writable);
// Force chalk to use colours, otherwise tests may pass when they were supposed to be failing.
chalk.level = 3;
});
const createLogger = options => {
const logger = new Logger(Object.assign({ outputStream }, options));
jest.spyOn(logger, 'log');
return logger;
};
describe('#log()', () => {
it('writes prefix + text to the output stream', () => {
const logger = new Logger({ outputStream });
logger.log('foo', 'bar');
expect(outputStream.write).toHaveBeenCalledTimes(2);
expect(outputStream.write).toHaveBeenCalledWith('foo');
expect(outputStream.write).toHaveBeenCalledWith('bar');
});
it('writes multiple lines of text with prefix on each', () => {
const logger = new Logger({ outputStream });
logger.log('foo', 'bar\nbaz\n');
expect(outputStream.write).toHaveBeenCalledTimes(2);
expect(outputStream.write).toHaveBeenCalledWith('foo');
expect(outputStream.write).toHaveBeenCalledWith('bar\nfoobaz\n');
});
it('does not prepend prefix if last call did not finish with a LF', () => {
const logger = new Logger({ outputStream });
logger.log('foo', 'bar');
outputStream.write.mockClear();
logger.log('foo', 'baz');
expect(outputStream.write).toHaveBeenCalledTimes(1);
expect(outputStream.write).toHaveBeenCalledWith('baz');
});
it('does not prepend prefix or handle text if logger is in raw mode', () => {
const logger = new Logger({ outputStream, raw: true });
logger.log('foo', 'bar\nbaz\n');
expect(outputStream.write).toHaveBeenCalledTimes(1);
expect(outputStream.write).toHaveBeenCalledWith('bar\nbaz\n');
});
});
describe('#logGlobalEvent()', () => {
it('does nothing if in raw mode', () => {
const logger = createLogger({ raw: true });
logger.logGlobalEvent('foo');
expect(logger.log).not.toHaveBeenCalled();
});
it('logs in gray dim style with arrow prefix', () => {
const logger = createLogger();
logger.logGlobalEvent('foo');
expect(logger.log).toHaveBeenCalledWith(
chalk.reset('-->') + ' ',
chalk.reset('foo') + '\n'
);
});
});
describe('#logCommandText()', () => {
it('logs with name if no prefixFormat is set', () => {
const logger = createLogger();
logger.logCommandText('foo', { name: 'bla' });
expect(logger.log).toHaveBeenCalledWith(chalk.reset('[bla]') + ' ', 'foo');
});
it('logs with index if no prefixFormat is set, and command has no name', () => {
const logger = createLogger();
logger.logCommandText('foo', { index: 2 });
expect(logger.log).toHaveBeenCalledWith(chalk.reset('[2]') + ' ', 'foo');
});
it('logs with prefixFormat set to pid', () => {
const logger = createLogger({ prefixFormat: 'pid' });
logger.logCommandText('foo', {
pid: 123,
info: {}
});
expect(logger.log).toHaveBeenCalledWith(chalk.reset('[123]') + ' ', 'foo');
});
it('logs with prefixFormat set to name', () => {
const logger = createLogger({ prefixFormat: 'name' });
logger.logCommandText('foo', { name: 'bar' });
expect(logger.log).toHaveBeenCalledWith(chalk.reset('[bar]') + ' ', 'foo');
});
it('logs with prefixFormat set to index', () => {
const logger = createLogger({ prefixFormat: 'index' });
logger.logCommandText('foo', { index: 3 });
expect(logger.log).toHaveBeenCalledWith(chalk.reset('[3]') + ' ', 'foo');
});
it('logs with prefixFormat set to time (with timestampFormat)', () => {
const logger = createLogger({ prefixFormat: 'time', timestampFormat: 'yyyy' });
logger.logCommandText('foo', {});
const year = new Date().getFullYear();
expect(logger.log).toHaveBeenCalledWith(chalk.reset(`[${year}]`) + ' ', 'foo');
});
it('logs with templated prefixFormat', () => {
const logger = createLogger({ prefixFormat: '{index}-{name}' });
logger.logCommandText('foo', { index: 0, name: 'bar' });
expect(logger.log).toHaveBeenCalledWith(chalk.reset('0-bar') + ' ', 'foo');
});
it('does not strip spaces from beginning or end of prefixFormat', () => {
const logger = createLogger({ prefixFormat: ' {index}-{name} ' });
logger.logCommandText('foo', { index: 0, name: 'bar' });
expect(logger.log).toHaveBeenCalledWith(chalk.reset(' 0-bar ') + ' ', 'foo');
});
it('logs with no prefix', () => {
const logger = createLogger({ prefixFormat: 'none' });
logger.logCommandText('foo', { command: 'echo foo' });
expect(logger.log).toHaveBeenCalledWith(chalk.reset(''), 'foo');
});
it('logs prefix using command line itself', () => {
const logger = createLogger({ prefixFormat: 'command' });
logger.logCommandText('foo', { command: 'echo foo' });
expect(logger.log).toHaveBeenCalledWith(chalk.reset('[echo foo]') + ' ', 'foo');
});
it('logs prefix using command line itself, capped at prefixLength bytes', () => {
const logger = createLogger({ prefixFormat: 'command', prefixLength: 6 });
logger.logCommandText('foo', { command: 'echo foo' });
expect(logger.log).toHaveBeenCalledWith(chalk.reset('[ec..oo]') + ' ', 'foo');
});
it('logs prefix using prefixColor from command', () => {
const logger = createLogger();
logger.logCommandText('foo', { prefixColor: 'blue', index: 1 });
expect(logger.log).toHaveBeenCalledWith(chalk.blue('[1]') + ' ', 'foo');
});
it('logs prefix in gray dim if prefixColor from command does not exist', () => {
const logger = createLogger();
logger.logCommandText('foo', { prefixColor: 'blue.fake', index: 1 });
expect(logger.log).toHaveBeenCalledWith(chalk.reset('[1]') + ' ', 'foo');
});
it('logs prefix using prefixColor from command if prefixColor is a hex value', () => {
const logger = createLogger();
const prefixColor = '#32bd8a';
logger.logCommandText('foo', {prefixColor, index: 1});
expect(logger.log).toHaveBeenCalledWith(chalk.hex(prefixColor)('[1]') + ' ', 'foo');
});
it('does nothing if command is hidden by name', () => {
const logger = createLogger({ hide: ['abc'] });
logger.logCommandText('foo', { name: 'abc' });
expect(logger.log).not.toHaveBeenCalled();
});
it('does nothing if command is hidden by index', () => {
const logger = createLogger({ hide: [3] });
logger.logCommandText('foo', { index: 3 });
expect(logger.log).not.toHaveBeenCalled();
});
});
describe('#logCommandEvent()', () => {
it('does nothing if in raw mode', () => {
const logger = createLogger({ raw: true });
logger.logCommandEvent('foo');
expect(logger.log).not.toHaveBeenCalled();
});
it('does nothing if command is hidden by name', () => {
const logger = createLogger({ hide: ['abc'] });
logger.logCommandEvent('foo', { name: 'abc' });
expect(logger.log).not.toHaveBeenCalled();
});
it('does nothing if command is hidden by index', () => {
const logger = createLogger({ hide: [3] });
logger.logCommandEvent('foo', { index: 3 });
expect(logger.log).not.toHaveBeenCalled();
});
it('logs text in gray dim', () => {
const logger = createLogger();
logger.logCommandEvent('foo', { index: 1 });
expect(logger.log).toHaveBeenCalledWith(chalk.reset('[1]') + ' ', chalk.reset('foo') + '\n');
});
});