WIP: A simple cli for daily tangled use cases and AI integration. This is for my personal use right now, but happy if others get mileage from it! :)
1import * as fs from 'node:fs/promises';
2import * as process from 'node:process';
3
4/**
5 * Read body content from various sources following GitHub CLI patterns
6 *
7 * @param bodyString - Direct body text from --body flag
8 * @param bodyFilePath - File path or '-' for stdin
9 * @returns Body content or undefined if no input provided
10 * @throws Error if both bodyString and bodyFilePath are provided
11 * @throws Error if file doesn't exist or cannot be read
12 */
13export async function readBodyInput(
14 bodyString?: string,
15 bodyFilePath?: string
16): Promise<string | undefined> {
17 // Error if both are provided
18 if (bodyString !== undefined && bodyFilePath !== undefined) {
19 throw new Error('Cannot specify both --body and --body-file. Choose one input method.');
20 }
21
22 // Direct string input (including empty string)
23 if (bodyString !== undefined) {
24 return bodyString;
25 }
26
27 // File or stdin input
28 if (bodyFilePath) {
29 // Read from stdin
30 if (bodyFilePath === '-') {
31 return await readFromStdin();
32 }
33
34 // Read from file
35 try {
36 const stats = await fs.stat(bodyFilePath);
37
38 if (stats.isDirectory()) {
39 throw new Error(`'${bodyFilePath}' is a directory, not a file`);
40 }
41
42 const content = await fs.readFile(bodyFilePath, 'utf-8');
43 return content;
44 } catch (error) {
45 if (error instanceof Error) {
46 // Re-throw our custom directory error
47 if (error.message.includes('is a directory')) {
48 throw error;
49 }
50
51 // Handle ENOENT (file not found)
52 if ('code' in error && error.code === 'ENOENT') {
53 throw new Error(`File not found: ${bodyFilePath}`);
54 }
55
56 // Handle EACCES (permission denied)
57 if ('code' in error && error.code === 'EACCES') {
58 throw new Error(`Permission denied: ${bodyFilePath}`);
59 }
60
61 throw new Error(`Failed to read file '${bodyFilePath}': ${error.message}`);
62 }
63
64 throw new Error(`Failed to read file '${bodyFilePath}': Unknown error`);
65 }
66 }
67
68 // No input provided
69 return undefined;
70}
71
72/**
73 * Read content from stdin
74 * @returns Content from stdin as string
75 */
76async function readFromStdin(): Promise<string> {
77 return new Promise((resolve, reject) => {
78 const chunks: Buffer[] = [];
79
80 process.stdin.on('data', (chunk: Buffer) => {
81 chunks.push(chunk);
82 });
83
84 process.stdin.on('end', () => {
85 const content = Buffer.concat(chunks).toString('utf-8');
86 resolve(content);
87 });
88
89 process.stdin.on('error', (error: Error) => {
90 reject(new Error(`Failed to read from stdin: ${error.message}`));
91 });
92
93 // Resume stdin in case it's paused
94 process.stdin.resume();
95 });
96}