Repository: sindresorhus/execa (js)

Issue:
Make `.killed === true` regardless of how it was killed
See: https://github.com/sindresorhus/execa/pull/48/files#r71061011


Pull request:
Fix `error.killed` and rename to `error.isTerminated`
Fixes #52.

Edit: one test is failing on Windows. Looking into it.

Patch:
diff --git a/docs/scripts.md b/docs/scripts.md
index 60848ad..5aaa1e7 100644
--- a/docs/scripts.md
+++ b/docs/scripts.md
@@ -500,7 +500,7 @@ const {
   failed,
   timedOut,
   isCanceled,
-  killed,
+  isTerminated,
 	// and other error-related properties: code, etc.
 } = await $({timeout: 1})`sleep 2`;
 // file:///home/me/code/execa/lib/kill.js:60
@@ -521,7 +521,7 @@ const {
 //   stderr: '',
 //   failed: true,
 //   isCanceled: false,
-//   killed: false
+//   isTerminated: false
 // }
 ```
 
diff --git a/index.d.ts b/index.d.ts
index 92af32c..253215a 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -265,7 +265,7 @@ export type CommonOptions<EncodingType extends EncodingOption = DefaultEncodingO
 	try {
 		await subprocess;
 	} catch (error) {
-		console.log(subprocess.killed); // true
+		console.log(error.isTerminated); // true
 		console.log(error.isCanceled); // true
 	}
 	```
@@ -388,12 +388,16 @@ export type ExecaReturnBase<StdoutStderrType extends StdoutStderrAll> = {
 	timedOut: boolean;
 
 	/**
-	Whether the process was killed.
+	Whether the process was terminated using either:
+		- `childProcess.kill()`.
+		- A signal sent by another process. This case is [not supported on Windows](https://nodejs.org/api/process.html#signal-events).
 	*/
-	killed: boolean;
+	isTerminated: boolean;
 
 	/**
-	The name of the signal that was used to terminate the process. For example, `SIGFPE`.
+	The name of the signal (like `SIGFPE`) that terminated the process using either:
+		- `childProcess.kill()`.
+		- A signal sent by another process. This case is [not supported on Windows](https://nodejs.org/api/process.html#signal-events).
 
 	If a signal terminated the process, this property is defined and included in the error message. Otherwise it is `undefined`.
 	*/
@@ -420,7 +424,7 @@ Result of a child process execution. On success this is a plain object. On failu
 
 The child process fails when:
 - its exit code is not `0`
-- it was killed with a signal
+- it was terminated with a signal
 - timing out
 - being canceled
 - there's not enough memory or there are already too many child processes
@@ -642,7 +646,7 @@ try {
 		failed: true,
 		timedOut: false,
 		isCanceled: false,
-		killed: false,
+		isTerminated: false,
 		cwd: '/path/to/cwd'
 	}
 	\*\/
@@ -722,7 +726,7 @@ try {
 		failed: true,
 		timedOut: false,
 		isCanceled: false,
-		killed: false,
+		isTerminated: false,
 		cwd: '/path/to/cwd'
 	}
 	\*\/
diff --git a/index.js b/index.js
index 0d95c02..294bf31 100644
--- a/index.js
+++ b/index.js
@@ -100,7 +100,6 @@ export function execa(file, args, options) {
 			parsed,
 			timedOut: false,
 			isCanceled: false,
-			killed: false,
 		}));
 		mergePromise(dummySpawned, errorPromise);
 		return dummySpawned;
@@ -134,7 +133,6 @@ export function execa(file, args, options) {
 				parsed,
 				timedOut,
 				isCanceled: context.isCanceled || (parsed.options.signal ? parsed.options.signal.aborted : false),
-				killed: spawned.killed,
 			});
 
 			if (!parsed.options.reject) {
@@ -154,7 +152,7 @@ export function execa(file, args, options) {
 			failed: false,
 			timedOut: false,
 			isCanceled: false,
-			killed: false,
+			isTerminated: false,
 		};
 	};
 
@@ -191,7 +189,6 @@ export function execaSync(file, args, options) {
 			parsed,
 			timedOut: false,
 			isCanceled: false,
-			killed: false,
 		});
 	}
 
@@ -211,7 +208,6 @@ export function execaSync(file, args, options) {
 			parsed,
 			timedOut: result.error && result.error.code === 'ETIMEDOUT',
 			isCanceled: false,
-			killed: result.signal !== null,
 		});
 
 		if (!parsed.options.reject) {
@@ -230,7 +226,7 @@ export function execaSync(file, args, options) {
 		failed: false,
 		timedOut: false,
 		isCanceled: false,
-		killed: false,
+		isTerminated: false,
 	};
 }
 
diff --git a/lib/error.js b/lib/error.js
index 5e80c52..129bd40 100644
--- a/lib/error.js
+++ b/lib/error.js
@@ -36,7 +36,6 @@ export const makeError = ({
 	escapedCommand,
 	timedOut,
 	isCanceled,
-	killed,
 	parsed: {options: {timeout, cwd = process.cwd()}},
 }) => {
 	// `signal` and `exitCode` emitted on `spawned.on('exit')` event can be `null`.
@@ -81,7 +80,7 @@ export const makeError = ({
 	error.failed = true;
 	error.timedOut = Boolean(timedOut);
 	error.isCanceled = isCanceled;
-	error.killed = killed && !timedOut;
+	error.isTerminated = signal !== undefined;
 
 	return error;
 };
diff --git a/readme.md b/readme.md
index 197a712..135c7fa 100644
--- a/readme.md
+++ b/readme.md
@@ -212,7 +212,7 @@ try {
 		failed: true,
 		timedOut: false,
 		isCanceled: false,
-		killed: false
+		isTerminated: false
 	}
 	*/
 }
@@ -370,7 +370,7 @@ Result of a child process execution. On success this is a plain object. On failu
 
 The child process [fails](#failed) when:
 - its [exit code](#exitcode) is not `0`
-- it was [killed](#killed) with a [signal](#signal)
+- it was [terminated](#isterminated) with a [signal](#signal)
 - [timing out](#timedout)
 - [being canceled](#iscanceled)
 - there's not enough memory or there are already too many child processes
@@ -440,17 +440,21 @@ Whether the process was canceled.
 
 You can cancel the spawned process using the [`signal`](#signal-1) option.
 
-#### killed
+#### isTerminated
 
 Type: `boolean`
 
-Whether the process was killed.
+Whether the process was terminated using either:
+  - [`childProcess.kill()`](#killsignal-options).
+  - A signal sent by another process. This case is [not supported on Windows](https://nodejs.org/api/process.html#signal-events).
 
 #### signal
 
 Type: `string | undefined`
 
-The name of the signal that was used to terminate the process. For example, `SIGFPE`.
+The name of the signal (like `SIGFPE`) that terminated the process using either:
+  - [`childProcess.kill()`](#killsignal-options).
+  - A signal sent by another process. This case is [not supported on Windows](https://nodejs.org/api/process.html#signal-events).
 
 If a signal terminated the process, this property is defined and included in the error message. Otherwise it is `undefined`.
 
@@ -789,7 +793,7 @@ setTimeout(() => {
 try {
 	await subprocess;
 } catch (error) {
-	console.log(subprocess.killed); // true
+	console.log(error.isTerminated); // true
 	console.log(error.isCanceled); // true
 }
 ```


Test patch:
diff --git a/index.test-d.ts b/index.test-d.ts
index c3a301a..f2d87e9 100644
--- a/index.test-d.ts
+++ b/index.test-d.ts
@@ -68,7 +68,7 @@ try {
 	expectType<boolean>(unicornsResult.failed);
 	expectType<boolean>(unicornsResult.timedOut);
 	expectType<boolean>(unicornsResult.isCanceled);
-	expectType<boolean>(unicornsResult.killed);
+	expectType<boolean>(unicornsResult.isTerminated);
 	expectType<string | undefined>(unicornsResult.signal);
 	expectType<string | undefined>(unicornsResult.signalDescription);
 	expectType<string>(unicornsResult.cwd);
@@ -83,7 +83,7 @@ try {
 	expectType<boolean>(execaError.failed);
 	expectType<boolean>(execaError.timedOut);
 	expectType<boolean>(execaError.isCanceled);
-	expectType<boolean>(execaError.killed);
+	expectType<boolean>(execaError.isTerminated);
 	expectType<string | undefined>(execaError.signal);
 	expectType<string | undefined>(execaError.signalDescription);
 	expectType<string>(execaError.cwd);
@@ -105,7 +105,7 @@ try {
 	expectType<boolean>(unicornsResult.failed);
 	expectType<boolean>(unicornsResult.timedOut);
 	expectError(unicornsResult.isCanceled);
-	expectType<boolean>(unicornsResult.killed);
+	expectType<boolean>(unicornsResult.isTerminated);
 	expectType<string | undefined>(unicornsResult.signal);
 	expectType<string | undefined>(unicornsResult.signalDescription);
 	expectType<string>(unicornsResult.cwd);
@@ -120,7 +120,7 @@ try {
 	expectType<boolean>(execaError.failed);
 	expectType<boolean>(execaError.timedOut);
 	expectError(execaError.isCanceled);
-	expectType<boolean>(execaError.killed);
+	expectType<boolean>(execaError.isTerminated);
 	expectType<string | undefined>(execaError.signal);
 	expectType<string | undefined>(execaError.signalDescription);
 	expectType<string>(execaError.cwd);
diff --git a/test/error.js b/test/error.js
index 7cf6ca8..7ef2aef 100644
--- a/test/error.js
+++ b/test/error.js
@@ -1,11 +1,9 @@
 import process from 'node:process';
-import childProcess from 'node:child_process';
-import {promisify} from 'node:util';
 import test from 'ava';
 import {execa, execaSync} from '../index.js';
 import {FIXTURES_DIR, setFixtureDir} from './helpers/fixtures-dir.js';
 
-const pExec = promisify(childProcess.exec);
+const isWindows = process.platform === 'win32';
 
 setFixtureDir();
 
@@ -20,7 +18,7 @@ test('stdout/stderr/all available on errors', async t => {
 	t.is(typeof all, 'string');
 });
 
-const WRONG_COMMAND = process.platform === 'win32'
+const WRONG_COMMAND = isWindows
 	? '\'wrong\' is not recognized as an internal or external command,\r\noperable program or batch file.'
 	: '';
 
@@ -91,61 +89,57 @@ test('failed is true on failure', async t => {
 	t.true(failed);
 });
 
-test('error.killed is true if process was killed directly', async t => {
+test('error.isTerminated is true if process was killed directly', async t => {
 	const subprocess = execa('forever.js');
 
 	subprocess.kill();
 
-	const {killed} = await t.throwsAsync(subprocess, {message: /was killed with SIGTERM/});
-	t.true(killed);
+	const {isTerminated} = await t.throwsAsync(subprocess, {message: /was killed with SIGTERM/});
+	t.true(isTerminated);
 });
 
-test('error.killed is false if process was killed indirectly', async t => {
+test('error.isTerminated is true if process was killed indirectly', async t => {
 	const subprocess = execa('forever.js');
 
 	process.kill(subprocess.pid, 'SIGINT');
 
 	// `process.kill()` is emulated by Node.js on Windows
-	const message = process.platform === 'win32' ? /failed with exit code 1/ : /was killed with SIGINT/;
-	const {killed} = await t.throwsAsync(subprocess, {message});
-	t.false(killed);
+	const message = isWindows ? /failed with exit code 1/ : /was killed with SIGINT/;
+	const {isTerminated} = await t.throwsAsync(subprocess, {message});
+	t.not(isTerminated, isWindows);
 });
 
-test('result.killed is false if not killed', async t => {
-	const {killed} = await execa('noop.js');
-	t.false(killed);
+test('result.isTerminated is false if not killed', async t => {
+	const {isTerminated} = await execa('noop.js');
+	t.false(isTerminated);
 });
 
-test('result.killed is false if not killed, in sync mode', t => {
-	const {killed} = execaSync('noop.js');
-	t.false(killed);
+test('result.isTerminated is false if not killed and childProcess.kill() was called', async t => {
+	const subprocess = execa('noop.js');
+	subprocess.kill(0);
+	t.true(subprocess.killed);
+	const {isTerminated} = await subprocess;
+	t.false(isTerminated);
 });
 
-test('result.killed is false on process error', async t => {
-	const {killed} = await t.throwsAsync(execa('wrong command'));
-	t.false(killed);
+test('result.isTerminated is false if not killed, in sync mode', t => {
+	const {isTerminated} = execaSync('noop.js');
+	t.false(isTerminated);
 });
 
-test('result.killed is false on process error, in sync mode', t => {
-	const {killed} = t.throws(() => {
-		execaSync('wrong command');
-	});
-	t.false(killed);
+test('result.isTerminated is false on process error', async t => {
+	const {isTerminated} = await t.throwsAsync(execa('wrong command'));
+	t.false(isTerminated);
 });
 
-if (process.platform === 'darwin') {
-	test('sanity check: child_process.exec also has killed.false if killed indirectly', async t => {
-		const promise = pExec('forever.js');
-
-		process.kill(promise.child.pid, 'SIGINT');
-
-		const error = await t.throwsAsync(promise);
-		t.truthy(error);
-		t.false(error.killed);
+test('result.isTerminated is false on process error, in sync mode', t => {
+	const {isTerminated} = t.throws(() => {
+		execaSync('wrong command');
 	});
-}
+	t.false(isTerminated);
+});
 
-if (process.platform !== 'win32') {
+if (!isWindows) {
 	test('error.signal is SIGINT', async t => {
 		const subprocess = execa('forever.js');
 
diff --git a/test/kill.js b/test/kill.js
index a995e7c..293b80a 100644
--- a/test/kill.js
+++ b/test/kill.js
@@ -82,16 +82,16 @@ test('execa() returns a promise with kill()', t => {
 });
 
 test('timeout kills the process if it times out', async t => {
-	const {killed, timedOut} = await t.throwsAsync(execa('noop.js', {timeout: 1}), {message: TIMEOUT_REGEXP});
-	t.false(killed);
+	const {isTerminated, timedOut} = await t.throwsAsync(execa('noop.js', {timeout: 1}), {message: TIMEOUT_REGEXP});
+	t.true(isTerminated);
 	t.true(timedOut);
 });
 
 test('timeout kills the process if it times out, in sync mode', async t => {
-	const {killed, timedOut} = await t.throws(() => {
+	const {isTerminated, timedOut} = await t.throws(() => {
 		execaSync('noop.js', {timeout: 1, message: TIMEOUT_REGEXP});
 	});
-	t.false(killed);
+	t.true(isTerminated);
 	t.true(timedOut);
 });
 
@@ -198,10 +198,12 @@ test('removes exit handler on exit', async t => {
 	t.false(included);
 });
 
-test('cancel method kills the subprocess', t => {
+test('cancel method kills the subprocess', async t => {
 	const subprocess = execa('node');
 	subprocess.cancel();
 	t.true(subprocess.killed);
+	const {isTerminated} = await t.throwsAsync(subprocess);
+	t.true(isTerminated);
 });
 
 test('result.isCanceled is false when spawned.cancel() isn\'t called (success)', async t => {