Comparing version 1.0.1 to 2.0.0
{ | ||
"name": "run-pty", | ||
"version": "1.0.1", | ||
"version": "2.0.0", | ||
"author": "Simon Lydell", | ||
@@ -28,16 +28,19 @@ "license": "MIT", | ||
"dependencies": { | ||
"colorette": "^1.2.1", | ||
"node-pty": "^0.9.0" | ||
}, | ||
"devDependencies": { | ||
"eslint": "7.4.0", | ||
"eslint-plugin-jest": "23.18.0", | ||
"jest": "26.1.0", | ||
"prettier": "2.0.5" | ||
"@types/jest": "26.0.8", | ||
"@typescript-eslint/eslint-plugin": "3.7.1", | ||
"@typescript-eslint/parser": "3.7.1", | ||
"eslint": "7.6.0", | ||
"eslint-plugin-jest": "23.20.0", | ||
"jest": "26.2.2", | ||
"prettier": "2.0.5", | ||
"typescript": "3.9.7" | ||
}, | ||
"scripts": { | ||
"start": "node run-pty.js % cat % false % echo hello world % ping localhost % node test-keys.js", | ||
"test": "prettier --check . && eslint . && jest", | ||
"start": "node run-pty.js % cat % false % echo hello world % ping localhost % node test-keys.js % node signals.js % node slow-kill.js % node slow-kill.js 2000 \"Shutting down…\" % make watch", | ||
"test": "prettier --check . && eslint . && tsc && jest", | ||
"prepublishOnly": "npm test" | ||
} | ||
} |
112
README.md
# run-pty | ||
In `bash` you can use `fg`, `bg`, `jobs` and <kbd>ctrl+z</kbd> to run several commands at the same time in the same terminal. But it’s not very user friendly. | ||
`run-pty` is a command line tool that lets you run several commands _concurrently_ and _interactively._ Show output for one command at a time. Kill all at once. Nothing more, nothing less. | ||
`run-pty` is a command line tool that lets you run several commands concurrently. Show output for one command at a time. Kill all at once. Nothing more, nothing less. | ||
It’s like [concurrently] but the command outputs aren’t mixed, and you can restart commands individually and interact with them. I bet you can do the same with [tmux] if you – and your team mates – feel like installing and learning it. In `bash` you can use `command1 & command2` together with `fg`, `bg`, `jobs` and <kbd>ctrl+z</kbd> to achieve a similar result, but run-pty tries to be easier to use, and cross-platform. | ||
<kbd>ctrl+z</kbd> shows the _dashboard,_ which gives you an overview of all your running commands and lets you switch between them. | ||
<kbd>ctrl+c</kbd> exits current/all commands. | ||
<kbd>ctrl+c</kbd> kills commands. | ||
A use case is running several watchers. Maybe one or two for frontend (webpack, Parcel, Sass), and one for backend (nodemon, TypeScript, or even some watcher for another programming language). | ||
A use case is running several watchers. Maybe one or two for frontend (webpack, Parcel, Sass), and one for backend (nodemon, or even some watcher for another programming language). | ||
## Example | ||
```json | ||
{ | ||
"scripts": { | ||
"watch:frontend": "webpack-dev-server", | ||
"watch:backend": "nodemon server/index.ts", | ||
"watch:all": "run-pty % npm run watch:frontend % npm run watch:backend" | ||
"start": "run-pty % npm run frontend % npm run backend", | ||
"frontend": "parcel watch index.html", | ||
"backend": "nodemon server.js" | ||
} | ||
@@ -24,10 +26,81 @@ } | ||
``` | ||
1 🟢 pid 78147 npm run 'watch:frontend' | ||
2 🔴 exit 1 npm run 'watch:backend' | ||
$ npm start | ||
1-2 switch command | ||
ctrl+c exit current/all | ||
ctrl+z this dashboard | ||
> @ start /Users/lydell/src/run-pty/demo | ||
> run-pty % npm run frontend % npm run backend | ||
``` | ||
➡️ | ||
``` | ||
[1] 🟢 pid 11084 npm run frontend | ||
[2] 🟢 pid 11085 npm run backend | ||
[1-2] focus command | ||
[ctrl+c] kill all | ||
``` | ||
➡️ <kbd>1</kbd> ️️➡️ | ||
``` | ||
🟢 npm run frontend | ||
> @ frontend /Users/lydell/src/run-pty/demo | ||
> parcel watch index.html --log-level 4 | ||
[9:51:27 AM]: Building... | ||
[9:51:27 AM]: Building index.html... | ||
[9:51:27 AM]: Built index.html... | ||
[9:51:27 AM]: Producing bundles... | ||
[9:51:27 AM]: Packaging... | ||
[9:51:27 AM]: ✨ Built in 67ms. | ||
[ctrl+c] kill | ||
[ctrl+z] dashboard | ||
▊ | ||
``` | ||
➡️ <kbd>ctrl+c</kbd> ➡️ | ||
``` | ||
🟢 npm run frontend | ||
> @ frontend /Users/lydell/src/run-pty/demo | ||
> parcel watch index.html --log-level 4 | ||
[9:51:27 AM]: Building... | ||
[9:51:27 AM]: Building index.html... | ||
[9:51:27 AM]: Built index.html... | ||
[9:51:27 AM]: Producing bundles... | ||
[9:51:27 AM]: Packaging... | ||
[9:51:27 AM]: ✨ Built in 67ms. | ||
⚪ npm run frontend | ||
exit 0 | ||
[enter] restart | ||
[ctrl+c] kill all | ||
[ctrl+z] dashboard | ||
``` | ||
➡️ <kbd>ctrl+z</kbd> ➡️ | ||
``` | ||
[1] ⚪ exit 0 npm run frontend | ||
[2] 🟢 pid 11085 npm run backend | ||
[1-2] focus command | ||
[ctrl+c] kill all | ||
``` | ||
➡️ <kbd>ctrl+c</kbd> ➡️ | ||
``` | ||
[1] ⚪ exit 0 npm run frontend | ||
[2] ⚪ exit 0 npm run backend | ||
$ ▊ | ||
``` | ||
## Installation | ||
@@ -44,2 +117,12 @@ | ||
## iTerm2 flicker | ||
[iTerm2] has a bug where the window flickers when clearing the screen without GPU rendering: <https://gitlab.com/gnachman/iterm2/-/issues/7677> | ||
GPU rendering seems to be enabled by default, as long as your computer is connected to power. | ||
You can enable GPU rendering always by toggling “Preferences > General > Magic > GPU Rendering + Advanced GPU Settings… > Disable GPU rendering when disconnected from power.” | ||
There might still be occasional flicker. Hopefully the iTerm2 developers will improve this some time. It does not happen in the standard Terminal app. | ||
## License | ||
@@ -49,3 +132,6 @@ | ||
[apiel/run-screen]: https://github.com/apiel/run-screen | ||
[concurrently]: https://github.com/kimmobrunfeldt/concurrently | ||
[iterm2]: https://www.iterm2.com/ | ||
[microsoft/node-pty]: https://github.com/microsoft/node-pty | ||
[apiel/run-screen]: https://github.com/apiel/run-screen | ||
[tmux]: https://github.com/tmux/tmux |
613
run-pty.js
@@ -5,8 +5,35 @@ #!/usr/bin/env node | ||
const colorette = require("colorette"); | ||
const pty = require("node-pty"); | ||
/** | ||
* @typedef { | ||
| { tag: "Running", terminal: import("node-pty").IPty } | ||
| { tag: "Killing", terminal: import("node-pty").IPty, slow: boolean } | ||
| { tag: "Exit", exitCode: number } | ||
} Status | ||
* | ||
* @typedef { | ||
| { tag: "Command", index: number } | ||
| { tag: "Dashboard" } | ||
} Current | ||
*/ | ||
// node-pty does not support kill signals on Windows. | ||
// This is the same check that node-pty uses. | ||
const IS_WINDOWS = process.platform === "win32"; | ||
const MAX_HISTORY_DEFAULT = 1000000; | ||
const MAX_HISTORY = (() => { | ||
const env = process.env.RUN_PTY_MAX_HISTORY; | ||
return env !== undefined && /^\d+$/.test(env) | ||
? Number(env) | ||
: MAX_HISTORY_DEFAULT; | ||
})(); | ||
const NO_COLOR = "NO_COLOR" in process.env; | ||
const KEYS = { | ||
kill: "ctrl+c", | ||
restart: "enter ", // Extra space for alignment. | ||
restart: "enter", | ||
dashboard: "ctrl+z", | ||
@@ -25,12 +52,69 @@ }; | ||
const HIDE_CURSOR = "\x1B[?25l"; | ||
const SHOW_CURSOR = "\x1B[?25h"; | ||
const DISABLE_ALTERNATE_SCREEN = "\x1B[?1049l"; | ||
const DISABLE_BRACKETED_PASTE_MODE = "\x1B[?2004l"; | ||
const RESET_COLOR = "\x1B[0m"; | ||
const CLEAR = IS_WINDOWS ? "\x1B[2J\x1B[0f" : "\x1B[2J\x1B[3J\x1B[H"; | ||
const runningIndicator = "🟢"; | ||
const killingIndicator = "⭕"; | ||
/** | ||
* @param {number} exitCode | ||
* @returns {string} | ||
*/ | ||
const exitIndicator = (exitCode) => (exitCode === 0 ? "⚪" : "🔴"); | ||
const shortcut = (string) => colorette.blue(colorette.bold(string)); | ||
/** | ||
* @param {string} string | ||
* @returns {string} | ||
*/ | ||
const bold = (string) => (NO_COLOR ? string : `\x1B[1m${string}${RESET_COLOR}`); | ||
const runPty = shortcut("run-pty"); | ||
const pc = colorette.gray("%"); | ||
const at = colorette.gray("@"); | ||
/** | ||
* @param {string} string | ||
* @returns {string} | ||
*/ | ||
const dim = (string) => (NO_COLOR ? string : `\x1B[2m${string}${RESET_COLOR}`); | ||
/** | ||
* @param {string} string | ||
* @param {{ pad?: boolean }} pad | ||
*/ | ||
const shortcut = (string, { pad = true } = {}) => | ||
dim("[") + | ||
bold(string) + | ||
dim("]") + | ||
(pad ? " ".repeat(Math.max(0, KEYS.kill.length - string.length)) : ""); | ||
const runPty = bold("run-pty"); | ||
const pc = dim("%"); | ||
const at = dim("@"); | ||
/** | ||
* @param {Array<string>} labels | ||
* @returns {string} | ||
*/ | ||
const summarizeLabels = (labels) => { | ||
const numLabels = labels.length; | ||
return LABEL_GROUPS.flatMap((group, index) => { | ||
const previousLength = LABEL_GROUPS.slice(0, index).reduce( | ||
(sum, previousGroup) => sum + previousGroup.length, | ||
0 | ||
); | ||
const currentLength = previousLength + group.length; | ||
return numLabels > previousLength | ||
? numLabels < currentLength | ||
? group.slice(0, numLabels - previousLength) | ||
: group | ||
: []; | ||
}) | ||
.map((group) => | ||
group.length === 1 ? group[0] : `${group[0]}-${group[group.length - 1]}` | ||
) | ||
.join("/"); | ||
}; | ||
const help = ` | ||
@@ -41,6 +125,6 @@ Run several commands concurrently. | ||
${shortcut(summarizeLabels(ALL_LABELS.split("")))} switch command | ||
${shortcut(summarizeLabels(ALL_LABELS.split("")))} focus command | ||
${shortcut(KEYS.dashboard)} dashboard | ||
${shortcut(KEYS.kill)} exit current/all | ||
${shortcut(KEYS.restart)} restart exited command | ||
${shortcut(KEYS.kill)} kill focused/all | ||
${shortcut(KEYS.restart)} restart killed/exited command | ||
@@ -54,9 +138,35 @@ Separate the commands with a character of choice: | ||
Note: All arguments are strings and passed as-is – no shell script execution. | ||
Use ${bold("sh -c '...'")} or similar if you need that. | ||
Environment variables: | ||
${bold("RUN_PTY_MAX_HISTORY")} | ||
Number of characters of output to remember. | ||
Higher → more command scrollback | ||
Lower → faster switching between commands | ||
Default: ${MAX_HISTORY_DEFAULT} | ||
${bold("NO_COLOR")} | ||
Disable colored output. | ||
`.trim(); | ||
function drawDashboard(commands, width) { | ||
/** | ||
* @param {Array<Command>} commands | ||
* @returns {string} | ||
*/ | ||
const killAllLabel = (commands) => | ||
commands.some((command) => command.status.tag === "Killing") | ||
? "force kill all" | ||
: commands.every((command) => command.status.tag === "Exit") | ||
? "exit" | ||
: "kill all"; | ||
/** | ||
* @param {Array<Command>} commands | ||
* @param {number} width | ||
* @param {boolean} attemptedKillAll | ||
*/ | ||
const drawDashboard = (commands, width, attemptedKillAll) => { | ||
const lines = commands.map((command) => [ | ||
colorette.bgWhite( | ||
colorette.black(colorette.bold(` ${command.label || " "} `)) | ||
), | ||
shortcut(command.label || " ", { pad: false }), | ||
statusText(command.status), | ||
@@ -79,18 +189,54 @@ command.name, | ||
if ( | ||
attemptedKillAll && | ||
commands.every((command) => command.status.tag === "Exit") | ||
) { | ||
return `${finalLines}\n`; | ||
} | ||
// Newlines at the end are wanted here. | ||
return ` | ||
${finalLines} | ||
${shortcut(padEnd(label, KEYS.kill.length))} switch command | ||
${shortcut(KEYS.kill)} exit current/all | ||
${shortcut(KEYS.dashboard)} this dashboard | ||
`.trim(); | ||
} | ||
${shortcut(label)} focus command | ||
${shortcut(KEYS.kill)} ${killAllLabel(commands)} | ||
`.trimStart(); | ||
}; | ||
function firstHistoryLine(name) { | ||
return `${runningIndicator} ${name}\n`; | ||
} | ||
/** | ||
* @param {string} name | ||
* @returns {string} | ||
*/ | ||
const firstHistoryLine = (name) => `${runningIndicator} ${name}\n`; | ||
// Newlines at start/end are wanted here. | ||
function exitText(commandName, exitCode) { | ||
return ` | ||
// Newlines at the start/end are wanted here. | ||
const runningText = ` | ||
${shortcut(KEYS.kill)} kill | ||
${shortcut(KEYS.dashboard)} dashboard | ||
`; | ||
/** | ||
* @param {string} commandName | ||
* @returns {string} | ||
*/ | ||
const killingText = (commandName) => | ||
// Newlines at the start/end are wanted here. | ||
` | ||
${killingIndicator} ${commandName} | ||
killing… | ||
${shortcut(KEYS.kill)} force kill | ||
${shortcut(KEYS.dashboard)} dashboard | ||
`; | ||
/** | ||
* @param {Array<Command>} commands | ||
* @param {string} commandName | ||
* @param {number} exitCode | ||
* @returns {string} | ||
*/ | ||
const exitText = (commands, commandName, exitCode) => | ||
// Newlines at the start/end are wanted here. | ||
` | ||
${exitIndicator(exitCode)} ${commandName} | ||
@@ -100,8 +246,11 @@ exit ${exitCode} | ||
${shortcut(KEYS.restart)} restart | ||
${shortcut(KEYS.kill)} exit all | ||
${shortcut(KEYS.kill)} ${killAllLabel(commands)} | ||
${shortcut(KEYS.dashboard)} dashboard | ||
`; | ||
} | ||
function statusText(status) { | ||
/** | ||
* @param {Status} status | ||
* @returns {string} | ||
*/ | ||
const statusText = (status) => { | ||
switch (status.tag) { | ||
@@ -111,21 +260,34 @@ case "Running": | ||
case "Killing": | ||
return `${killingIndicator} pid ${status.terminal.pid}`; | ||
case "Exit": | ||
return `${exitIndicator(status.exitCode)} exit ${status.exitCode}`; | ||
default: | ||
throw new Error("Unknown command status", status); | ||
} | ||
} | ||
}; | ||
function removeColor(string) { | ||
/** | ||
* @param {string} string | ||
* @returns {string} | ||
*/ | ||
const removeColor = (string) => | ||
// eslint-disable-next-line no-control-regex | ||
return string.replace(/\x1b\[\d+m/g, ""); | ||
} | ||
string.replace(/\x1B\[\d+m/g, ""); | ||
function truncate(string, maxLength) { | ||
/** | ||
* @param {string} string | ||
* @param {number} maxLength | ||
* @returns {string} | ||
*/ | ||
const truncate = (string, maxLength) => { | ||
const diff = removeColor(string).length - maxLength; | ||
return diff <= 0 ? string : `${string.slice(0, -(diff + 2))}…`; | ||
} | ||
}; | ||
function padEnd(string, maxLength) { | ||
/** | ||
* @param {string} string | ||
* @param {number} maxLength | ||
* @returns {string} | ||
*/ | ||
const padEnd = (string, maxLength) => { | ||
const chars = Array.from(string); | ||
@@ -137,34 +299,28 @@ return chars | ||
.join(""); | ||
} | ||
}; | ||
function commandToPresentationName(command) { | ||
return command | ||
/** | ||
* @param {Array<string>} command | ||
* @returns {string} | ||
*/ | ||
const commandToPresentationName = (command) => | ||
command | ||
.map((part) => | ||
/^[\w./-]+$/.test(part) ? part : `'${part.replace(/'/g, "’")}'` | ||
/^[\w.,:/=@%+-]+$/.test(part) ? part : `'${part.replace(/'/g, "’")}'` | ||
) | ||
.join(" "); | ||
} | ||
function summarizeLabels(labels) { | ||
const numLabels = labels.length; | ||
return LABEL_GROUPS.map((group, index) => { | ||
const previousLength = LABEL_GROUPS.slice(0, index).reduce( | ||
(sum, previousGroup) => sum + previousGroup.length, | ||
0 | ||
); | ||
const currentLength = previousLength + group.length; | ||
return numLabels > previousLength | ||
? numLabels < currentLength | ||
? group.slice(0, numLabels - previousLength) | ||
: group | ||
: undefined; | ||
}) | ||
.filter(Boolean) | ||
.map((group) => | ||
group.length === 1 ? group[0] : `${group[0]}-${group[group.length - 1]}` | ||
) | ||
.join("/"); | ||
} | ||
/** | ||
* @typedef { | ||
| { tag: "Help" } | ||
| { tag: "Error", message: string } | ||
| { tag: "Parsed", commands: Array<Array<string>> } | ||
} ParseResult | ||
*/ | ||
function parseArgs(args) { | ||
/** | ||
* @param {Array<string>} args | ||
* @returns {ParseResult} | ||
*/ | ||
const parseArgs = (args) => { | ||
if (args.length === 0 || args[0] === "-h" || args[0] === "--help") { | ||
@@ -216,5 +372,14 @@ return { tag: "Help" }; | ||
}; | ||
} | ||
}; | ||
class Command { | ||
/** | ||
* @param {{ | ||
label: string, | ||
file: string, | ||
args: Array<string>, | ||
onData: (data: string) => undefined, | ||
onExit: () => undefined, | ||
}} commandInit | ||
*/ | ||
constructor({ label, file, args, onData, onExit }) { | ||
@@ -227,3 +392,5 @@ this.label = label; | ||
this.onExit = onExit; | ||
this.history = []; | ||
/** @type {string} */ | ||
this.history = ""; | ||
/** @type {Status} */ | ||
this.status = { tag: "Exit", exitCode: 0 }; | ||
@@ -233,10 +400,13 @@ this.start(); | ||
/** | ||
* @returns {void} | ||
*/ | ||
start() { | ||
if (this.status.tag === "Running") { | ||
if (this.status.tag !== "Exit") { | ||
throw new Error( | ||
`pty already running with pid ${this.status.terminal.pid} for: ${this.name}` | ||
`Cannot start pty with pid ${this.status.terminal.pid} because not exited for: ${this.name}` | ||
); | ||
} | ||
this.history = [firstHistoryLine(this.name)]; | ||
this.history = firstHistoryLine(this.name); | ||
@@ -249,3 +419,3 @@ const terminal = pty.spawn(this.file, this.args, { | ||
const disposeOnData = terminal.onData((data) => { | ||
this.history.push(data); | ||
this.pushHistory(data); | ||
this.onData(data); | ||
@@ -258,3 +428,3 @@ }); | ||
this.status = { tag: "Exit", exitCode }; | ||
this.onExit(exitCode); | ||
this.onExit(); | ||
}); | ||
@@ -265,39 +435,147 @@ | ||
log(data) { | ||
this.history.push(data); | ||
this.onData(data); | ||
/** | ||
* @returns {undefined} | ||
*/ | ||
kill() { | ||
// https://www.gnu.org/software/libc/manual/html_node/Termination-Signals.html | ||
switch (this.status.tag) { | ||
case "Running": | ||
this.status = { | ||
tag: "Killing", | ||
terminal: this.status.terminal, | ||
slow: false, | ||
}; | ||
setTimeout(() => { | ||
if (this.status.tag === "Killing") { | ||
this.status.slow = true; | ||
// Ugly way to redraw: | ||
this.onData(""); | ||
} | ||
}, 100); | ||
if (IS_WINDOWS) { | ||
this.status.terminal.kill(); | ||
} else { | ||
// SIGHUP causes a silent exit for `npm run`. | ||
this.status.terminal.kill("SIGHUP"); | ||
// SIGTERM is needed for some programs (but is noisy for `npm run`). | ||
this.status.terminal.kill("SIGTERM"); | ||
} | ||
return undefined; | ||
case "Killing": | ||
if (IS_WINDOWS) { | ||
this.status.terminal.kill(); | ||
} else { | ||
this.status.terminal.kill("SIGKILL"); | ||
} | ||
return undefined; | ||
case "Exit": | ||
throw new Error(`Cannot kill already exited pty for: ${this.name}`); | ||
} | ||
} | ||
/** | ||
* @param {string} data | ||
* @returns {void} | ||
*/ | ||
pushHistory(data) { | ||
this.history += data; | ||
if (this.history.length > MAX_HISTORY) { | ||
this.history = this.history.slice(-MAX_HISTORY); | ||
} | ||
} | ||
} | ||
function runCommands(rawCommands) { | ||
/** | ||
* @param {Array<Array<string>>} rawCommands | ||
*/ | ||
const runCommands = (rawCommands) => { | ||
/** @type {Current} */ | ||
let current = { tag: "Dashboard" }; | ||
let attemptedKillAll = false; | ||
const printHistory = (command) => { | ||
for (const data of command.history) { | ||
process.stdout.write(data); | ||
/** | ||
* @param {Command} command | ||
* @returns {undefined} | ||
*/ | ||
const printHistoryAndExtraText = (command) => { | ||
process.stdout.write( | ||
SHOW_CURSOR + | ||
DISABLE_ALTERNATE_SCREEN + | ||
RESET_COLOR + | ||
CLEAR + | ||
command.history | ||
); | ||
switch (command.status.tag) { | ||
case "Running": | ||
if (command.history.endsWith("\n")) { | ||
process.stdout.write(RESET_COLOR + runningText); | ||
} | ||
return undefined; | ||
case "Killing": | ||
if (command.status.slow) { | ||
process.stdout.write( | ||
HIDE_CURSOR + RESET_COLOR + killingText(command.name) | ||
); | ||
} | ||
return undefined; | ||
case "Exit": | ||
process.stdout.write( | ||
HIDE_CURSOR + | ||
RESET_COLOR + | ||
exitText(commands, command.name, command.status.exitCode) | ||
); | ||
return undefined; | ||
} | ||
}; | ||
/** | ||
* @returns {void} | ||
*/ | ||
const switchToDashboard = () => { | ||
current = { tag: "Dashboard" }; | ||
console.clear(); | ||
console.log(drawDashboard(commands, process.stdout.columns)); | ||
process.stdout.write( | ||
HIDE_CURSOR + | ||
DISABLE_ALTERNATE_SCREEN + | ||
RESET_COLOR + | ||
CLEAR + | ||
drawDashboard(commands, process.stdout.columns, attemptedKillAll) | ||
); | ||
}; | ||
/** | ||
* @param {number} index | ||
* @returns {void} | ||
*/ | ||
const switchToCommand = (index) => { | ||
const command = commands[index]; | ||
current = { tag: "Command", index }; | ||
console.clear(); | ||
printHistory(command); | ||
printHistoryAndExtraText(command); | ||
}; | ||
/** | ||
* @returns {void} | ||
*/ | ||
const killAll = () => { | ||
for (const command of commands) { | ||
if (command.status.tag === "Running") { | ||
command.status.terminal.kill(); | ||
attemptedKillAll = true; | ||
const notExited = commands.filter( | ||
(command) => command.status.tag !== "Exit" | ||
); | ||
if (notExited.length === 0) { | ||
switchToDashboard(); | ||
process.exit(0); | ||
} else { | ||
for (const command of notExited) { | ||
command.kill(); | ||
} | ||
// So you can see how killing other commands go: | ||
switchToDashboard(); | ||
} | ||
process.exit(0); | ||
}; | ||
/** @type {Array<Command>} */ | ||
const commands = rawCommands.map( | ||
@@ -311,12 +589,46 @@ ([file, ...args], index) => | ||
if (current.tag === "Command" && current.index === index) { | ||
process.stdout.write(data); | ||
const command = commands[current.index]; | ||
switch (command.status.tag) { | ||
case "Running": | ||
process.stdout.write(data); | ||
return undefined; | ||
case "Killing": | ||
// Redraw with killingText at the bottom. | ||
printHistoryAndExtraText(command); | ||
return undefined; | ||
case "Exit": | ||
throw new Error( | ||
`Received unexpected output from already exited pty for: ${command.name}\n${data}` | ||
); | ||
} | ||
} else { | ||
return undefined; | ||
} | ||
}, | ||
onExit: (exitCode) => { | ||
const command = commands[index]; | ||
command.log(exitText(command.name, exitCode)); | ||
if (current.tag === "Dashboard") { | ||
// Redraw dashboard. | ||
onExit: () => { | ||
// Exit the whole program if all commands are killed. | ||
if ( | ||
attemptedKillAll && | ||
commands.every((command2) => command2.status.tag === "Exit") | ||
) { | ||
switchToDashboard(); | ||
process.exit(0); | ||
} | ||
switch (current.tag) { | ||
case "Command": | ||
if (current.index === index) { | ||
const command = commands[index]; | ||
// Redraw current command. | ||
printHistoryAndExtraText(command); | ||
} | ||
return undefined; | ||
case "Dashboard": | ||
// Redraw dashboard. | ||
switchToDashboard(); | ||
return undefined; | ||
} | ||
}, | ||
@@ -343,7 +655,6 @@ }) | ||
process.stdin.setRawMode(true); | ||
process.stdin.setEncoding("utf8"); | ||
process.stdin.on("data", (data) => { | ||
onStdin( | ||
data, | ||
data.toString("utf8"), | ||
current, | ||
@@ -354,6 +665,34 @@ commands, | ||
killAll, | ||
printHistory | ||
printHistoryAndExtraText | ||
); | ||
}); | ||
// Clean up all commands if someone tries to kill run-pty. | ||
for (const signal of ["SIGHUP", "SIGINT", "SIGTERM"]) { | ||
process.on(signal, killAll); | ||
} | ||
// Don’t leave running processes behind in case of an unexpected error. | ||
for (const event of ["uncaughtException", "unhandledRejection"]) { | ||
process.on(event, (error) => { | ||
console.error(error); | ||
for (const command of commands) { | ||
if (command.status.tag !== "Exit") { | ||
if (IS_WINDOWS) { | ||
command.status.terminal.kill(); | ||
} else { | ||
command.status.terminal.kill("SIGKILL"); | ||
} | ||
} | ||
} | ||
process.exit(1); | ||
}); | ||
} | ||
process.on("exit", () => { | ||
process.stdout.write( | ||
SHOW_CURSOR + DISABLE_BRACKETED_PASTE_MODE + RESET_COLOR | ||
); | ||
}); | ||
if (commands.length === 1) { | ||
@@ -364,5 +703,15 @@ switchToCommand(0); | ||
} | ||
} | ||
}; | ||
function onStdin( | ||
/** | ||
* @param {string} data | ||
* @param {Current} current | ||
* @param {Array<Command>} commands | ||
* @param {() => void} switchToDashboard | ||
* @param {(index: number) => void} switchToCommand | ||
* @param {() => void} killAll | ||
* @param {(command: Command) => void} printHistoryAndExtraText | ||
* @returns {undefined} | ||
*/ | ||
const onStdin = ( | ||
data, | ||
@@ -374,4 +723,4 @@ current, | ||
killAll, | ||
printHistory | ||
) { | ||
printHistoryAndExtraText | ||
) => { | ||
switch (current.tag) { | ||
@@ -384,15 +733,28 @@ case "Command": { | ||
case KEY_CODES.kill: | ||
command.status.terminal.kill(); | ||
break; | ||
command.kill(); | ||
return undefined; | ||
case KEY_CODES.dashboard: | ||
switchToDashboard(); | ||
break; | ||
return undefined; | ||
default: | ||
command.status.terminal.write(data); | ||
break; | ||
return undefined; | ||
} | ||
break; | ||
case "Killing": | ||
switch (data) { | ||
case KEY_CODES.kill: | ||
command.kill(); | ||
return undefined; | ||
case KEY_CODES.dashboard: | ||
switchToDashboard(); | ||
return undefined; | ||
default: | ||
return undefined; | ||
} | ||
case "Exit": | ||
@@ -402,20 +764,17 @@ switch (data) { | ||
killAll(); | ||
break; | ||
return undefined; | ||
case KEY_CODES.dashboard: | ||
switchToDashboard(); | ||
break; | ||
return undefined; | ||
case KEY_CODES.restart: | ||
command.start(); | ||
command.history.unshift("\n"); | ||
printHistory(command); | ||
break; | ||
printHistoryAndExtraText(command); | ||
return undefined; | ||
default: | ||
return undefined; | ||
} | ||
break; | ||
default: | ||
throw new Error("Unknown command status", command); | ||
} | ||
break; | ||
} | ||
@@ -427,3 +786,3 @@ | ||
killAll(); | ||
break; | ||
return undefined; | ||
@@ -437,13 +796,12 @@ default: { | ||
} | ||
break; | ||
return undefined; | ||
} | ||
} | ||
break; | ||
default: | ||
throw new Error("Unknown current", current); | ||
} | ||
} | ||
}; | ||
function run() { | ||
/** | ||
* @returns {undefined} | ||
*/ | ||
const run = () => { | ||
if (!process.stdin.isTTY) { | ||
@@ -462,7 +820,6 @@ console.error( | ||
process.exit(0); | ||
break; | ||
case "Parsed": | ||
runCommands(parseResult.commands); | ||
break; | ||
return undefined; | ||
@@ -472,11 +829,6 @@ case "Error": | ||
process.exit(1); | ||
break; | ||
default: | ||
console.error("Unknown parseResult", parseResult); | ||
process.exit(1); | ||
break; | ||
} | ||
} | ||
}; | ||
// @ts-ignore | ||
if (require.main === module) { | ||
@@ -491,3 +843,2 @@ run(); | ||
drawDashboard, | ||
exitText, | ||
help, | ||
@@ -494,0 +845,0 @@ parseArgs, |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances in 1 package
27174
1
5
712
135
8
3
1
- Removedcolorette@^1.2.1
- Removedcolorette@1.4.0(transitive)