Skip to content

Commit d278b99

Browse files
authored
fix: tty support (#337)
* fix: tty support * chore: changeset
1 parent 540565f commit d278b99

4 files changed

Lines changed: 42 additions & 6 deletions

File tree

.changeset/late-sides-wonder.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'dotenv-diff': patch
3+
---
4+
5+
fixed interactive prompt may run in non-TTY environments

packages/cli/src/commands/prompts/prompts.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ export async function confirmYesNo(
1717
): Promise<boolean> {
1818
if (isYesMode) return true;
1919
if (isCiMode) return false;
20+
21+
// Avoid hanging in non-interactive environments (e.g. hooks/scripts without TTY)
22+
const hasInteractiveTty = Boolean(
23+
process.stdin?.isTTY && process.stdout?.isTTY,
24+
);
25+
if (!hasInteractiveTty) return false;
26+
2027
const res = await prompts({
2128
type: 'select',
2229
name: 'ok',

packages/cli/test/e2e/cli.compare.e2e.test.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ describe('added .env to gitignore with --compare and --fix', () => {
5656
fs.mkdirSync(path.join(cwd, 'src'), { recursive: true });
5757
fs.writeFileSync(
5858
path.join(cwd, 'src', 'index.ts'),
59-
`const apiKey = process.env.API_KEY;`.trimStart(),
59+
'const apiKey = process.env.API_KEY;'.trimStart(),
6060
);
6161

6262
const res = runCli(cwd, ['--compare', '--fix']);
@@ -78,7 +78,7 @@ describe('added .env to gitignore with --compare and --fix', () => {
7878
fs.mkdirSync(path.join(cwd, 'src'), { recursive: true });
7979
fs.writeFileSync(
8080
path.join(cwd, 'src', 'index.ts'),
81-
`const apiKey = process.env.API_KEY;`.trimStart(),
81+
'const apiKey = process.env.API_KEY;'.trimStart(),
8282
);
8383

8484
const res = runCli(cwd, ['--compare', '--json']);
@@ -89,17 +89,15 @@ describe('added .env to gitignore with --compare and --fix', () => {
8989
expect(json[0].gitignoreIssue?.reason).toBe('not-ignored');
9090
});
9191

92-
it('Will prompt .env.example file not found. and prompt if .env.local exists and .env.example is set to --example', async () => {
92+
it('Will skip interactive prompt in non-TTY when .env.example is missing and --example is set', async () => {
9393
const cwd = tmpDir();
9494
fs.writeFileSync(path.join(cwd, '.env.local'), 'FOO=bar');
9595

9696
const res = runCli(cwd, ['--compare', '--example', '.env.example']);
9797

9898
expect(res.status).toBe(0);
9999
expect(res.stdout).toContain('▸ File not found');
100-
expect(res.stdout).toContain(
101-
'Do you want to create a .env.example file from .env.local?',
102-
);
100+
expect(res.stdout).toContain('▸ Skipping .env.example creation');
103101
});
104102

105103
describe('Values mismatch checks', () => {

packages/cli/test/unit/commands/prompts/prompts.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,17 @@ vi.mock('prompts');
66

77
const mockPrompts = prompts as unknown as ReturnType<typeof vi.fn>;
88

9+
function setTTY(stdinTTY: boolean, stdoutTTY: boolean): void {
10+
Object.defineProperty(process.stdin, 'isTTY', {
11+
value: stdinTTY,
12+
configurable: true,
13+
});
14+
Object.defineProperty(process.stdout, 'isTTY', {
15+
value: stdoutTTY,
16+
configurable: true,
17+
});
18+
}
19+
920
describe('confirmYesNo', () => {
1021
beforeEach(() => {
1122
vi.clearAllMocks();
@@ -32,6 +43,7 @@ describe('confirmYesNo', () => {
3243
});
3344

3445
it('returns true when user selects Yes', async () => {
46+
setTTY(true, true);
3547
mockPrompts.mockResolvedValue({ ok: true });
3648

3749
const result = await confirmYesNo('Are you sure?', {
@@ -44,6 +56,7 @@ describe('confirmYesNo', () => {
4456
});
4557

4658
it('returns false when user selects No', async () => {
59+
setTTY(true, true);
4760
mockPrompts.mockResolvedValue({ ok: false });
4861

4962
const result = await confirmYesNo('Are you sure?', {
@@ -56,6 +69,7 @@ describe('confirmYesNo', () => {
5669
});
5770

5871
it('returns false when prompt returns undefined', async () => {
72+
setTTY(true, true);
5973
mockPrompts.mockResolvedValue({});
6074

6175
const result = await confirmYesNo('Are you sure?', {
@@ -65,4 +79,16 @@ describe('confirmYesNo', () => {
6579

6680
expect(result).toBe(false);
6781
});
82+
83+
it('returns false when no TTY is available and does not prompt', async () => {
84+
setTTY(false, false);
85+
86+
const result = await confirmYesNo('Are you sure?', {
87+
isCiMode: false,
88+
isYesMode: false,
89+
});
90+
91+
expect(result).toBe(false);
92+
expect(mockPrompts).not.toHaveBeenCalled();
93+
});
6894
});

0 commit comments

Comments
 (0)