Repository: sindresorhus/execa (js)

Issue:
Add `cwd` to error
## Goal

Add `cwd` of command to error context 

## Current status

Currently error context does not include `cwd`:
https://github.com/sindresorhus/execa/blob/7bc6eb41018e0a8553add519559055c7e7b6f322/lib/error.js#L27

## Motivation

When running a similar command for a lot of directories, it is hard to diagnose which specific command/directory pair is in fact the culprit.

For instance one could run the following pseudo-code:

```
// my-dir
//  - my-dir-1
//  - my-dir-2
//  - my-dir-3
fs.readdirSync(testFolder).forEach(async file => {
  await execa('[[ $(pwd) = '/my-dir/my-dir-1' ]]', { cwd: dirname(file) };
});
```

It may be hard to figure out which specific invocation failed.

Reporting the `cwd` would help to diagnose the issue.

## Drawbacks

This may unintentionally lead to leak some details about the file system folders if the error is reported as-is

Pull request description:
feat(error.js): Add `cwd` error property
Fixes #564

(I couldn't find any contribution guidelines, so I hope everything is okay)

@ehmicky I let you review this

Patch:
diff --git a/index.d.ts b/index.d.ts
index bbf581a..c9cb28f 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -364,6 +364,11 @@ export type ExecaReturnBase<StdoutStderrType extends StdoutStderrAll> = {
 	If a signal terminated the process, this property is defined and included in the error message. Otherwise it is `undefined`. It is also `undefined` when the signal is very uncommon which should seldomly happen.
 	*/
 	signalDescription?: string;
+
+	/**
+	The `cwd` of the command if provided in the command options. Otherwise it is `process.cwd()`.
+	*/
+	cwd: string;
 };
 
 export type ExecaSyncReturnValue<StdoutStderrType extends StdoutStderrAll = string> = {
@@ -596,7 +601,8 @@ try {
 		failed: true,
 		timedOut: false,
 		isCanceled: false,
-		killed: false
+		killed: false,
+		cwd: '/path/to/cwd'
 	}
 	\*\/
 }
@@ -679,7 +685,8 @@ try {
 		failed: true,
 		timedOut: false,
 		isCanceled: false,
-		killed: false
+		killed: false,
+		cwd: '/path/to/cwd'
 	}
 	\*\/
 }
diff --git a/lib/error.js b/lib/error.js
index b12c144..5e80c52 100644
--- a/lib/error.js
+++ b/lib/error.js
@@ -1,3 +1,4 @@
+import process from 'node:process';
 import {signalsByName} from 'human-signals';
 
 const getErrorPrefix = ({timedOut, timeout, errorCode, signal, signalDescription, exitCode, isCanceled}) => {
@@ -36,7 +37,7 @@ export const makeError = ({
 	timedOut,
 	isCanceled,
 	killed,
-	parsed: {options: {timeout}},
+	parsed: {options: {timeout, cwd = process.cwd()}},
 }) => {
 	// `signal` and `exitCode` emitted on `spawned.on('exit')` event can be `null`.
 	// We normalize them to `undefined`
@@ -67,6 +68,7 @@ export const makeError = ({
 	error.signalDescription = signalDescription;
 	error.stdout = stdout;
 	error.stderr = stderr;
+	error.cwd = cwd;
 
 	if (all !== undefined) {
 		error.all = all;
diff --git a/readme.md b/readme.md
index 553c368..2975b39 100644
--- a/readme.md
+++ b/readme.md
@@ -8,6 +8,36 @@
 
 > Process execution for humans
 
+<br>
+
+---
+
+<div align="center">
+	<p>
+		<p>
+			<sup>
+				<a href="https://github.com/sponsors/sindresorhus">Sindre's open source work is supported by the community</a>
+			</sup>
+		</p>
+		<sup>Special thanks to:</sup>
+		<br>
+		<br>
+		<a href="https://transloadit.com">
+			<picture>
+				<source width="360" media="(prefers-color-scheme: dark)" srcset="https://sindresorhus.com/assets/thanks/transloadit-logo-dark.svg">
+				<source width="360" media="(prefers-color-scheme: light)" srcset="https://sindresorhus.com/assets/thanks/transloadit-logo.svg">
+				<img width="360" src="https://sindresorhus.com/assets/thanks/transloadit-logo.svg" alt="Transloadit logo">
+			</picture>
+		</a>
+		<br>
+		<br>
+	</p>
+</div>
+
+---
+
+<br>
+
 ## Why
 
 This package improves [`child_process`](https://nodejs.org/api/child_process.html) methods with:
@@ -431,6 +461,12 @@ A human-friendly description of the signal that was used to terminate the proces
 
 If a signal terminated the process, this property is defined and included in the error message. Otherwise it is `undefined`. It is also `undefined` when the signal is very uncommon which should seldomly happen.
 
+#### cwd
+
+Type: `string`
+
+The `cwd` of the command if provided in the [command options](#cwd-1). Otherwise it is `process.cwd()`.
+
 #### message
 
 Type: `string`


Test patch:
diff --git a/index.test-d.ts b/index.test-d.ts
index 284545b..46e3572 100644
--- a/index.test-d.ts
+++ b/index.test-d.ts
@@ -71,6 +71,7 @@ try {
 	expectType<boolean>(unicornsResult.killed);
 	expectType<string | undefined>(unicornsResult.signal);
 	expectType<string | undefined>(unicornsResult.signalDescription);
+	expectType<string>(unicornsResult.cwd);
 } catch (error: unknown) {
 	const execaError = error as ExecaError;
 
@@ -85,6 +86,7 @@ try {
 	expectType<boolean>(execaError.killed);
 	expectType<string | undefined>(execaError.signal);
 	expectType<string | undefined>(execaError.signalDescription);
+	expectType<string>(execaError.cwd);
 	expectType<string>(execaError.shortMessage);
 	expectType<string | undefined>(execaError.originalMessage);
 }
@@ -106,6 +108,7 @@ try {
 	expectType<boolean>(unicornsResult.killed);
 	expectType<string | undefined>(unicornsResult.signal);
 	expectType<string | undefined>(unicornsResult.signalDescription);
+	expectType<string>(unicornsResult.cwd);
 } catch (error: unknown) {
 	const execaError = error as ExecaSyncError;
 
@@ -120,6 +123,7 @@ try {
 	expectType<boolean>(execaError.killed);
 	expectType<string | undefined>(execaError.signal);
 	expectType<string | undefined>(execaError.signalDescription);
+	expectType<string>(execaError.cwd);
 	expectType<string>(execaError.shortMessage);
 	expectType<string | undefined>(execaError.originalMessage);
 }
diff --git a/test/error.js b/test/error.js
index 36bc0ec..3d7c124 100644
--- a/test/error.js
+++ b/test/error.js
@@ -3,7 +3,7 @@ import childProcess from 'node:child_process';
 import {promisify} from 'node:util';
 import test from 'ava';
 import {execa, execaSync} from '../index.js';
-import {setFixtureDir} from './helpers/fixtures-dir.js';
+import {FIXTURES_DIR, setFixtureDir} from './helpers/fixtures-dir.js';
 
 const pExec = promisify(childProcess.exec);
 
@@ -212,3 +212,14 @@ test('error.code is defined on failure if applicable', async t => {
 	const {code} = await t.throwsAsync(execa('noop.js', {cwd: 1}));
 	t.is(code, 'ERR_INVALID_ARG_TYPE');
 });
+
+test('error.cwd is defined on failure if applicable', async t => {
+	const {cwd} = await t.throwsAsync(execa('noop-throw.js', [], {cwd: FIXTURES_DIR}));
+	t.is(cwd, FIXTURES_DIR);
+});
+
+test('error.cwd is undefined on failure if not passed as options', async t => {
+	const expectedCwd = process.cwd();
+	const {cwd} = await t.throwsAsync(execa('noop-throw.js'));
+	t.is(cwd, expectedCwd);
+});