From 1b91bb6e18cd2eb1734f43b74c92dbd265240354 Mon Sep 17 00:00:00 2001 From: "heecheol.park" Date: Tue, 12 May 2026 21:43:35 +0900 Subject: [PATCH] fix(convert): read stdin as a stream and guard against TTY-only invocations `fs.readFileSync(process.stdin.fd, 'utf-8')` is fragile across platforms and silently hangs when the command is run interactively without piped input. Replace it with an async stream consumer and surface a clear error when stdin is a TTY. --- bin/confluence.js | 15 ++++++++++++++- tests/convert.test.js | 18 ++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/bin/confluence.js b/bin/confluence.js index afe1d81..3fe20b7 100755 --- a/bin/confluence.js +++ b/bin/confluence.js @@ -47,6 +47,15 @@ function handleCommandError(analytics, commandName, error, onExtra = null) { process.exit(1); } +async function readStdin() { + process.stdin.setEncoding('utf8'); + let data = ''; + for await (const chunk of process.stdin) { + data += chunk; + } + return data; +} + // Wraps a command action with the standard analytics + client + error pipeline. // The handler still calls analytics.track(name, true) on success so it can opt // into alternative tracking keys (e.g. *_cancel, *_dry_run). @@ -1987,7 +1996,11 @@ program if (options.inputFile) { input = fs.readFileSync(options.inputFile, 'utf-8'); } else { - input = fs.readFileSync(process.stdin.fd, 'utf-8'); + if (process.stdin.isTTY) { + console.error(chalk.red('Error: No input provided. Use --input-file or pipe content via stdin.')); + process.exit(1); + } + input = await readStdin(); } const converter = ConfluenceClient.createLocalConverter(); diff --git a/tests/convert.test.js b/tests/convert.test.js index c96c21c..840715c 100644 --- a/tests/convert.test.js +++ b/tests/convert.test.js @@ -73,6 +73,24 @@ describe('convert command', () => { expect(output).toContain('World'); }); + test('reads input from stdin when --input-file is omitted', () => { + const output = run( + ['convert', '--input-format', 'markdown', '--output-format', 'storage'], + '# Piped\n\nbody\n' + ); + expect(output).toContain('

'); + expect(output).toContain('Piped'); + expect(output).toContain('body'); + }); + + test('handles empty stdin without hanging or crashing', () => { + const output = run( + ['convert', '--input-format', 'markdown', '--output-format', 'storage'], + '' + ); + expect(output).toBe(''); + }); + test('markdown to storage via files', () => { const inputFile = writeInput('input.md', '# Test\n\nParagraph\n'); const outputFile = path.join(tmpDir, 'output.xml');