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(
20 'Cannot specify both --body and --body-file. Choose one input method.',
21 );
22 }
23
24 // Direct string input (including empty string)
25 if (bodyString !== undefined) {
26 return bodyString;
27 }
28
29 // File or stdin input
30 if (bodyFilePath) {
31 // Read from stdin
32 if (bodyFilePath === '-') {
33 return await readFromStdin();
34 }
35
36 // Read from file
37 try {
38 const stats = await fs.stat(bodyFilePath);
39
40 if (stats.isDirectory()) {
41 throw new Error(`'${bodyFilePath}' is a directory, not a file`);
42 }
43
44 const content = await fs.readFile(bodyFilePath, 'utf-8');
45 return content;
46 } catch (error) {
47 if (error instanceof Error) {
48 // Re-throw our custom directory error
49 if (error.message.includes('is a directory')) {
50 throw error;
51 }
52
53 // Handle ENOENT (file not found)
54 if ('code' in error && error.code === 'ENOENT') {
55 throw new Error(`File not found: ${bodyFilePath}`);
56 }
57
58 // Handle EACCES (permission denied)
59 if ('code' in error && error.code === 'EACCES') {
60 throw new Error(`Permission denied: ${bodyFilePath}`);
61 }
62
63 throw new Error(
64 `Failed to read file '${bodyFilePath}': ${error.message}`,
65 );
66 }
67
68 throw new Error(`Failed to read file '${bodyFilePath}': Unknown error`);
69 }
70 }
71
72 // No input provided
73 return undefined;
74}
75
76/**
77 * Read content from stdin
78 * @returns Content from stdin as string
79 */
80async function readFromStdin(): Promise<string> {
81 return new Promise((resolve, reject) => {
82 const chunks: Buffer[] = [];
83
84 process.stdin.on('data', (chunk: Buffer) => {
85 chunks.push(chunk);
86 });
87
88 process.stdin.on('end', () => {
89 const content = Buffer.concat(chunks).toString('utf-8');
90 resolve(content);
91 });
92
93 process.stdin.on('error', (error: Error) => {
94 reject(new Error(`Failed to read from stdin: ${error.message}`));
95 });
96
97 // Resume stdin in case it's paused
98 process.stdin.resume();
99 });
100}