diff --git a/build/core.cjs b/build/core.cjs index ce58696523..b305fd3972 100644 --- a/build/core.cjs +++ b/build/core.cjs @@ -574,9 +574,17 @@ var _ProcessPromise = class _ProcessPromise extends Promise { )) || cb(); }, on: { - start: () => { + start: (child) => { + var _a2; $2.log({ kind: "cmd", cmd: $2.cmd, cwd, verbose: self.isVerbose(), id }); self.timeout($2.timeout, $2.timeoutSignal); + if (!$2.input && !self.sync && (child == null ? void 0 : child.stdin) && !child.stdin.destroyed && ((_a2 = import_node_process2.default.stdin) == null ? void 0 : _a2.readable) && !import_node_process2.default.stdin.readableEnded && _ProcessPromise.bus.sources(self).length === 0) { + import_node_process2.default.stdin.pipe(child.stdin); + child.stdin.on("error", import_util.noop); + child.once("close", () => { + if (child.stdin) import_node_process2.default.stdin.unpipe(child.stdin); + }); + } }, stdout: (data) => { $2.log({ kind: "stdout", data, verbose: !self._piped && self.isVerbose(), id }); diff --git a/src/core.ts b/src/core.ts index 53caaee342..f08c794ae5 100644 --- a/src/core.ts +++ b/src/core.ts @@ -348,9 +348,29 @@ export class ProcessPromise extends Promise { ) || cb() }, on: { - start: () => { + start: (child) => { $.log({ kind: 'cmd', cmd: $.cmd, cwd, verbose: self.isVerbose(), id }) self.timeout($.timeout, $.timeoutSignal) + + // Forward process.stdin to the child when no explicit input is + // provided and the process isn't receiving piped input from another + // process. This enables interactive commands like `$\`cat\`` to + // read from the terminal, matching standard shell behavior. + if ( + !$.input && + !self.sync && + child?.stdin && + !child.stdin.destroyed && + process.stdin?.readable && + !process.stdin.readableEnded && + ProcessPromise.bus.sources(self).length === 0 + ) { + process.stdin.pipe(child.stdin) + child.stdin.on('error', noop) + child.once('close', () => { + if (child.stdin) process.stdin.unpipe(child.stdin) + }) + } }, stdout: (data) => { // If the process is piped, don't print its output. diff --git a/test/core.test.js b/test/core.test.js index 80d96f62c7..f7323c1b88 100644 --- a/test/core.test.js +++ b/test/core.test.js @@ -19,7 +19,7 @@ import { basename } from 'node:path' import { WriteStream } from 'node:fs' import { Readable, Transform, Writable } from 'node:stream' import { Socket } from 'node:net' -import { ChildProcess } from 'node:child_process' +import { ChildProcess, spawn as cpSpawn } from 'node:child_process' import { $, ProcessPromise, @@ -642,6 +642,32 @@ describe('core', () => { assert.equal((await p).stdout, 'bar') }) + test('forwards process.stdin to child when no input is provided', async () => { + const buildPath = process.cwd() + '/build/core.js' + const script = tempfile( + 'stdin-forward-test.mjs', + `import {$} from '${buildPath}'\n` + + 'const r = await $`cat`\n' + + 'process.stdout.write(r.stdout)\n' + ) + + const child = cpSpawn(process.execPath, [script], { + stdio: ['pipe', 'pipe', 'pipe'], + }) + child.stdin.write('hello from stdin\n') + child.stdin.end() + + const chunks = [] + child.stdout.on('data', (d) => chunks.push(d)) + + const code = await new Promise((resolve) => child.on('close', resolve)) + const stdout = Buffer.concat(chunks).toString() + + assert.equal(code, 0) + assert.equal(stdout.trim(), 'hello from stdin') + await fs.rm(script) + }) + describe('pipe()', () => { test('accepts Writable', async () => { let contents = ''