Skip to content

Commit 98317c1

Browse files
committed
Enhance CLI functionality to support environment variables from stdin
- Introduced the --env-stdin option to read environment variables directly from stdin. - Updated usage instructions to reflect the new option and clarify mutual exclusivity with --env-file. - Added error handling for multiple --env-stdin specifications and conflicts with --env-file. - Implemented tests to verify the new functionality and error conditions.
1 parent 9dc9d65 commit 98317c1

File tree

3 files changed

+157
-9
lines changed

3 files changed

+157
-9
lines changed

src/cli.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { execFile } from 'child_process';
22
import fs from 'fs';
33
import path from 'path';
44
import os from 'os';
5+
import { spawn } from 'child_process';
56

67
describe('envmcp CLI', () => {
78
const defaultEnvFile = path.join(process.cwd(), '.env.mcp');
@@ -94,4 +95,74 @@ describe('envmcp CLI', () => {
9495
done();
9596
});
9697
}, 10000);
98+
99+
it('should read environment variables from stdin when --env-stdin is specified', (done) => {
100+
const child = spawn(process.execPath, [
101+
path.join(process.cwd(), 'dist/cli.js'),
102+
'--env-stdin',
103+
'node',
104+
'-p',
105+
'process.env.STDIN_VAR'
106+
]);
107+
108+
let stdout = '';
109+
let stderr = '';
110+
111+
child.stdout.on('data', (data) => {
112+
stdout += data.toString();
113+
});
114+
115+
child.stderr.on('data', (data) => {
116+
stderr += data.toString();
117+
});
118+
119+
child.on('close', (code) => {
120+
if (code !== 0) {
121+
console.error('Test error - stderr:', stderr);
122+
done.fail(new Error(`Process exited with code ${code}`));
123+
return;
124+
}
125+
expect(stdout.trim()).toBe('stdin_value');
126+
done();
127+
});
128+
129+
// Write environment variables to stdin
130+
child.stdin.write('STDIN_VAR=stdin_value\n');
131+
child.stdin.end();
132+
}, 10000);
133+
134+
it('should error when --env-file and --env-stdin are both specified', (done) => {
135+
const args = [
136+
path.join(process.cwd(), 'dist/cli.js'),
137+
'--env-file',
138+
customEnvFile,
139+
'--env-stdin',
140+
'node',
141+
'-p',
142+
'process.env.TEST_VAR'
143+
];
144+
145+
execFile(process.execPath, args, (error, stdout, stderr) => {
146+
expect(error).not.toBeNull();
147+
expect(stderr).toContain('--env-file and --env-stdin are mutually exclusive');
148+
done();
149+
});
150+
}, 10000);
151+
152+
it('should error when --env-stdin is specified multiple times', (done) => {
153+
const args = [
154+
path.join(process.cwd(), 'dist/cli.js'),
155+
'--env-stdin',
156+
'--env-stdin',
157+
'node',
158+
'-p',
159+
'process.env.TEST_VAR'
160+
];
161+
162+
execFile(process.execPath, args, (error, stdout, stderr) => {
163+
expect(error).not.toBeNull();
164+
expect(stderr).toContain('--env-stdin option specified more than once');
165+
done();
166+
});
167+
}, 10000);
97168
});

src/cli.ts

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
11
#!/usr/bin/env node
22

3-
import { loadEnvironmentVariablesFromFile, executeCommand } from './index';
3+
import { loadEnvironmentVariablesFromFile, loadEnvironmentVariablesFromStdin, executeCommand } from './index';
44

55
function printUsage() {
6-
console.error('Usage: envmcp [--env-file <path>] <command> [args...]');
6+
console.error('Usage: envmcp [--env-file <path>] [--env-stdin] <command> [args...]');
77
console.error('\nOptions:');
88
console.error(' -e, --env-file <path> Specify a custom path to the environment file');
9+
console.error(' --env-stdin Read environment variables from stdin');
910
console.error('\nArguments:');
1011
console.error(' <command> The command to execute');
1112
console.error(' [args...] Arguments for the command');
1213
console.error('\nNote: All options must precede the command.');
14+
console.error('Note: --env-file and --env-stdin are mutually exclusive.');
1315
}
1416

15-
function main() {
17+
async function main() {
1618
const argv = process.argv.slice(2);
1719
let customEnvFilePath: string | undefined = undefined;
20+
let useStdin = false;
1821
let commandIndex = 0;
1922
let error = false;
2023

@@ -27,13 +30,31 @@ function main() {
2730
error = true;
2831
break;
2932
}
33+
if (useStdin) {
34+
console.error('Error: --env-file and --env-stdin are mutually exclusive.');
35+
error = true;
36+
break;
37+
}
3038
if (commandIndex + 1 >= argv.length || argv[commandIndex + 1].startsWith('-')) {
3139
console.error(`Error: ${arg} option requires a path.`);
3240
error = true;
3341
break;
3442
}
3543
customEnvFilePath = argv[commandIndex + 1];
3644
commandIndex += 2;
45+
} else if (arg === '--env-stdin') {
46+
if (useStdin) {
47+
console.error('Error: --env-stdin option specified more than once.');
48+
error = true;
49+
break;
50+
}
51+
if (customEnvFilePath !== undefined) {
52+
console.error('Error: --env-file and --env-stdin are mutually exclusive.');
53+
error = true;
54+
break;
55+
}
56+
useStdin = true;
57+
commandIndex += 1;
3758
} else {
3859
// First non-option argument marks the start of the command
3960
break;
@@ -53,8 +74,15 @@ function main() {
5374
const command = commandArgs[0];
5475
const actualArgsForCommand = commandArgs.slice(1);
5576

56-
// Load environment variables from .env.mcp file
57-
if (!loadEnvironmentVariablesFromFile(customEnvFilePath)) {
77+
// Load environment variables from .env.mcp file or stdin
78+
let envLoaded = false;
79+
if (useStdin) {
80+
envLoaded = await loadEnvironmentVariablesFromStdin();
81+
} else {
82+
envLoaded = loadEnvironmentVariablesFromFile(customEnvFilePath);
83+
}
84+
85+
if (!envLoaded) {
5886
process.exit(1);
5987
}
6088

src/index.ts

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,11 @@ export function findEnvFilePath(startDir: string = process.cwd()): string | unde
5959
}
6060

6161
/**
62-
* Parses an environment file and returns its contents as key-value pairs
63-
* @param filePath Path to the environment file
62+
* Parses environment content (from file or stdin) and returns key-value pairs
63+
* @param content The content string to parse
6464
* @returns Object containing environment variables
6565
*/
66-
function parseEnvFile(filePath: string): Record<string, string> {
67-
const content = fs.readFileSync(filePath, 'utf8');
66+
function parseEnvContent(content: string): Record<string, string> {
6867
const result: Record<string, string> = {};
6968

7069
// Split by newlines and process each line
@@ -96,6 +95,16 @@ function parseEnvFile(filePath: string): Record<string, string> {
9695
return result;
9796
}
9897

98+
/**
99+
* Parses an environment file and returns its contents as key-value pairs
100+
* @param filePath Path to the environment file
101+
* @returns Object containing environment variables
102+
*/
103+
function parseEnvFile(filePath: string): Record<string, string> {
104+
const content = fs.readFileSync(filePath, 'utf8');
105+
return parseEnvContent(content);
106+
}
107+
99108
/**
100109
* Loads environment variables from a .env.mcp file
101110
* @param filePath Optional custom path to the env file
@@ -124,6 +133,46 @@ export function loadEnvironmentVariablesFromFile(filePath?: string): boolean {
124133
}
125134
}
126135

136+
/**
137+
* Reads environment variables from stdin
138+
* @returns Promise that resolves to true if environment variables were loaded, false otherwise
139+
*/
140+
export function loadEnvironmentVariablesFromStdin(): Promise<boolean> {
141+
return new Promise((resolve) => {
142+
let stdinContent = '';
143+
144+
process.stdin.setEncoding('utf8');
145+
146+
process.stdin.on('readable', () => {
147+
let chunk;
148+
while ((chunk = process.stdin.read()) !== null) {
149+
stdinContent += chunk;
150+
}
151+
});
152+
153+
process.stdin.on('end', () => {
154+
try {
155+
const envVars = parseEnvContent(stdinContent);
156+
157+
// Add variables to process.env
158+
Object.entries(envVars).forEach(([key, value]) => {
159+
process.env[key] = value;
160+
});
161+
162+
resolve(true);
163+
} catch (error) {
164+
reportError(`Error parsing environment variables from stdin:`, error);
165+
resolve(false);
166+
}
167+
});
168+
169+
process.stdin.on('error', (error) => {
170+
reportError(`Error reading from stdin:`, error);
171+
resolve(false);
172+
});
173+
});
174+
}
175+
127176
/**
128177
* Executes a command with the given arguments
129178
* @param command The command to execute

0 commit comments

Comments
 (0)