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');