From 9a129af056f64768ec000fa22c836466b3874c60 Mon Sep 17 00:00:00 2001 From: "posthog[bot]" <206114724+posthog[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 13:48:18 +0000 Subject: [PATCH] fix: avoid EISDIR crash when .env is a directory readApiKeyFromEnv guarded each candidate path with fs.existsSync, which returns true for directories too, so a project with a directory named .env or .env.local (common with Python virtualenvs) crashed the subsequent readFileSync with EISDIR. Read directly and skip on any read error instead. Generated-By: PostHog Code Task-Id: 2e86216f-b802-4c88-88b5-0118cea5c89a --- src/utils/__tests__/env-api-key.test.ts | 58 +++++++++++++++++++++++++ src/utils/env-api-key.ts | 19 +++++--- 2 files changed, 71 insertions(+), 6 deletions(-) create mode 100644 src/utils/__tests__/env-api-key.test.ts diff --git a/src/utils/__tests__/env-api-key.test.ts b/src/utils/__tests__/env-api-key.test.ts new file mode 100644 index 00000000..e1ed75d8 --- /dev/null +++ b/src/utils/__tests__/env-api-key.test.ts @@ -0,0 +1,58 @@ +import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'fs'; +import path from 'path'; +import { tmpdir } from 'os'; +import { readApiKeyFromEnv } from '@utils/env-api-key'; + +describe('readApiKeyFromEnv', () => { + let tmpDir: string; + let cwdSpy: jest.SpyInstance; + + beforeEach(() => { + tmpDir = mkdtempSync(path.join(tmpdir(), 'wizard-env-test-')); + cwdSpy = jest.spyOn(process, 'cwd').mockReturnValue(tmpDir); + }); + + afterEach(() => { + cwdSpy.mockRestore(); + rmSync(tmpDir, { recursive: true, force: true }); + }); + + it('returns undefined when no env file exists', () => { + expect(readApiKeyFromEnv()).toBeUndefined(); + }); + + it('reads the key from .env', () => { + writeFileSync( + path.join(tmpDir, '.env'), + 'POSTHOG_PERSONAL_API_KEY=phx_from_env\n', + ); + expect(readApiKeyFromEnv()).toBe('phx_from_env'); + }); + + it('prefers .env.local over .env', () => { + writeFileSync( + path.join(tmpDir, '.env'), + 'POSTHOG_PERSONAL_API_KEY=phx_from_env\n', + ); + writeFileSync( + path.join(tmpDir, '.env.local'), + 'POSTHOG_PERSONAL_API_KEY=phx_from_local\n', + ); + expect(readApiKeyFromEnv()).toBe('phx_from_local'); + }); + + it('does not crash when .env is a directory (e.g. a Python virtualenv)', () => { + mkdirSync(path.join(tmpDir, '.env')); + expect(() => readApiKeyFromEnv()).not.toThrow(); + expect(readApiKeyFromEnv()).toBeUndefined(); + }); + + it('falls back to .env when .env.local is a directory', () => { + mkdirSync(path.join(tmpDir, '.env.local')); + writeFileSync( + path.join(tmpDir, '.env'), + 'POSTHOG_PERSONAL_API_KEY=phx_from_env\n', + ); + expect(readApiKeyFromEnv()).toBe('phx_from_env'); + }); +}); diff --git a/src/utils/env-api-key.ts b/src/utils/env-api-key.ts index 2eae97dc..5c92f680 100644 --- a/src/utils/env-api-key.ts +++ b/src/utils/env-api-key.ts @@ -9,12 +9,19 @@ export function readApiKeyFromEnv(): string | undefined { const envFiles = ['.env.local', '.env']; for (const envFile of envFiles) { const envPath = path.join(process.cwd(), envFile); - if (fs.existsSync(envPath)) { - const content = fs.readFileSync(envPath, 'utf8'); - const match = content.match(/^POSTHOG_PERSONAL_API_KEY=(.+)$/m); - if (match) { - return match[1].trim(); - } + let content: string; + try { + // `.env`/`.env.local` is occasionally a directory (e.g. a Python + // virtualenv named `.env`), so read directly and skip on failure rather + // than guarding with existsSync, which returns true for directories and + // lets readFileSync throw EISDIR. + content = fs.readFileSync(envPath, 'utf8'); + } catch { + continue; + } + const match = content.match(/^POSTHOG_PERSONAL_API_KEY=(.+)$/m); + if (match) { + return match[1].trim(); } } return undefined;