Skip to content

Commit a4e7011

Browse files
committed
fix: Skip open() for some linux envs
1 parent 5712486 commit a4e7011

File tree

9 files changed

+54
-30
lines changed

9 files changed

+54
-30
lines changed

cli/src/chat.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { AnalyticsEvent } from '@codebuff/common/constants/analytics-events'
22
import type { FeedbackCategory } from '@codebuff/common/constants/feedback'
3-
import open from 'open'
3+
import { safeOpen } from './utils/open-url'
44
import {
55
useCallback,
66
useEffect,
@@ -1158,7 +1158,7 @@ export const Chat = ({
11581158
return
11591159
}
11601160
// Otherwise open the buy credits page
1161-
open(WEBSITE_URL + '/usage')
1161+
safeOpen(WEBSITE_URL + '/usage')
11621162
},
11631163
}),
11641164
[

cli/src/commands/command-registry.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { CHATGPT_OAUTH_ENABLED } from '@codebuff/common/constants/chatgpt-oauth'
22
import { CLAUDE_OAUTH_ENABLED } from '@codebuff/common/constants/claude-oauth'
3-
import open from 'open'
3+
import { safeOpen } from '../utils/open-url'
44

55
import { handleAdsEnable, handleAdsDisable } from './ads'
66
import { buildInterviewPrompt, buildPlanPrompt, buildReviewPromptFromArgs } from './prompt-builders'
@@ -407,7 +407,7 @@ const ALL_COMMANDS: CommandDefinition[] = [
407407
name: 'subscribe',
408408
aliases: ['strong', 'sub', 'buy-credits'],
409409
handler: (params) => {
410-
open(WEBSITE_URL + '/subscribe')
410+
safeOpen(WEBSITE_URL + '/subscribe')
411411
clearInput(params)
412412
},
413413
}),

cli/src/components/ad-banner.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import { TextAttributes } from '@opentui/core'
2-
import open from 'open'
2+
import { safeOpen } from '../utils/open-url'
33
import React, { useState } from 'react'
44

55
import { Button } from './button'
66
import { Clickable } from './clickable'
77
import { useTerminalDimensions } from '../hooks/use-terminal-dimensions'
88
import { useTheme } from '../hooks/use-theme'
99
import { IS_FREEBUFF } from '../utils/constants'
10-
import { logger } from '../utils/logger'
1110

1211
import type { AdResponse } from '../hooks/use-gravity-ad'
1312

@@ -49,9 +48,7 @@ export const AdBanner: React.FC<AdBannerProps> = ({ ad, onDisableAds, isFreeMode
4948
const handleAdMouseOut = () => setIsLinkHovered(false)
5049
const handleAdClick = () => {
5150
if (ad.clickUrl) {
52-
open(ad.clickUrl).catch((err) => {
53-
logger.error(err, 'Failed to open ad link')
54-
})
51+
safeOpen(ad.clickUrl)
5552
}
5653
}
5754

cli/src/components/subscription-limit-banner.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { SUBSCRIPTION_TIERS } from '@codebuff/common/constants/subscription-plans'
22
import { IS_FREEBUFF } from '../utils/constants'
3-
import open from 'open'
3+
import { safeOpen } from '../utils/open-url'
44
import React from 'react'
55

66
import { Button } from './button'
@@ -61,11 +61,11 @@ export const SubscriptionLimitBanner = () => {
6161
}
6262

6363
const handleBuyCredits = () => {
64-
open(WEBSITE_URL + '/usage')
64+
safeOpen(WEBSITE_URL + '/usage')
6565
}
6666

6767
const handleUpgrade = () => {
68-
open(WEBSITE_URL + '/subscribe')
68+
safeOpen(WEBSITE_URL + '/subscribe')
6969
}
7070

7171
const borderColor = isWeeklyLimit ? theme.error : theme.warning

cli/src/components/usage-banner.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { CLAUDE_OAUTH_ENABLED } from '@codebuff/common/constants/claude-oauth'
33
import { IS_FREEBUFF } from '../utils/constants'
44
import { isChatGptOAuthValid, isClaudeOAuthValid } from '@codebuff/sdk'
55
import { TextAttributes } from '@opentui/core'
6-
import open from 'open'
6+
import { safeOpen } from '../utils/open-url'
77
import React, { useEffect, useMemo } from 'react'
88

99
import { BottomBanner } from './bottom-banner'
@@ -135,7 +135,7 @@ export const UsageBanner = ({ showTime }: { showTime: number }) => {
135135
{/* Codebuff credits section - structured layout */}
136136
<Button
137137
onClick={() => {
138-
open(WEBSITE_URL + '/usage')
138+
safeOpen(WEBSITE_URL + '/usage')
139139
}}
140140
>
141141
<box style={{ flexDirection: 'column', gap: 0 }}>

cli/src/hooks/use-fetch-login-url.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useMutation } from '@tanstack/react-query'
2-
import open from 'open'
2+
import { safeOpen } from '../utils/open-url'
33

44
import { WEBSITE_URL } from '../login/constants'
55
import { generateLoginUrl } from '../login/login-flow'
@@ -45,12 +45,7 @@ export function useFetchLoginUrl({
4545
setHasOpenedBrowser(true)
4646

4747
// Open browser after fetching URL
48-
try {
49-
await open(data.loginUrl)
50-
} catch (err) {
51-
logger.error(err, 'Failed to open browser')
52-
// Don't show error, user can still click the URL
53-
}
48+
await safeOpen(data.loginUrl)
5449
},
5550
onError: (err) => {
5651
setError(err instanceof Error ? err.message : 'Failed to get login URL')

cli/src/utils/chatgpt-oauth.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
resetChatGptOAuthRateLimit,
2020
saveChatGptOAuthCredentials,
2121
} from '@codebuff/sdk'
22-
import open from 'open'
22+
import { safeOpen } from './open-url'
2323

2424
import type { ChatGptOAuthCredentials } from '@codebuff/sdk'
2525

@@ -218,12 +218,7 @@ export function connectChatGptOAuth(): {
218218
const { codeVerifier, authUrl } = startChatGptOAuthFlow()
219219
const credentials = startCallbackServer(codeVerifier)
220220

221-
open(authUrl).catch(() => {
222-
console.debug(
223-
'Failed to open browser for ChatGPT OAuth. Manual URL:',
224-
authUrl,
225-
)
226-
})
221+
void safeOpen(authUrl)
227222

228223
return { authUrl, credentials }
229224
}

cli/src/utils/claude-oauth.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
isClaudeOAuthValid,
1313
resetClaudeOAuthRateLimit,
1414
} from '@codebuff/sdk'
15-
import open from 'open'
15+
import { safeOpen } from './open-url'
1616

1717
import type { ClaudeOAuthCredentials } from '@codebuff/sdk'
1818

@@ -78,7 +78,7 @@ export function startOAuthFlow(): { codeVerifier: string; authUrl: string } {
7878
*/
7979
export async function openOAuthInBrowser(): Promise<string> {
8080
const { authUrl, codeVerifier } = startOAuthFlow()
81-
await open(authUrl)
81+
await safeOpen(authUrl)
8282
return codeVerifier
8383
}
8484

cli/src/utils/open-url.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import os from 'os'
2+
3+
import open from 'open'
4+
5+
import { logger } from './logger'
6+
7+
/**
8+
* Safely open a URL in the user's default browser.
9+
*
10+
* On headless Linux (no DISPLAY or WAYLAND_DISPLAY), calling `open()` spawns
11+
* `xdg-open` which can crash the entire process — even inside a try/catch —
12+
* because the child process may trigger fatal signals. This wrapper detects
13+
* headless environments and skips the call entirely.
14+
*
15+
* @returns `true` if the browser was (likely) opened, `false` if skipped.
16+
*/
17+
export async function safeOpen(url: string): Promise<boolean> {
18+
if (os.platform() === 'linux') {
19+
const hasDisplay = Boolean(
20+
process.env.DISPLAY || process.env.WAYLAND_DISPLAY,
21+
)
22+
if (!hasDisplay) {
23+
logger.warn(
24+
'No display server detected (DISPLAY / WAYLAND_DISPLAY unset). Skipping browser open.',
25+
)
26+
return false
27+
}
28+
}
29+
30+
try {
31+
await open(url)
32+
return true
33+
} catch (err) {
34+
logger.error(err, 'Failed to open browser')
35+
return false
36+
}
37+
}

0 commit comments

Comments
 (0)