Skip to content

Commit da353f5

Browse files
committed
Merge branch 'main' into gpt-5.2
2 parents 11a15cf + 1cba17d commit da353f5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1916
-524
lines changed

.bin/bun

Lines changed: 1 addition & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -202,46 +202,10 @@ check_env_setup() {
202202
echo "" >&2
203203
}
204204

205-
# Clear inherited env vars that would conflict with .env file values
206-
# Bun loads .env files natively, but inherited shell variables take precedence.
207-
# We unset variables defined in our env files so Bun can set fresh values.
208-
clear_inherited_env_vars() {
209-
# Skip clearing if CODEBUFF_SKIP_ENV_CLEAR is set
210-
# Used by TypeScript scripts that already have env vars loaded via Bun
211-
if [ -n "$CODEBUFF_SKIP_ENV_CLEAR" ]; then
212-
return
213-
fi
214-
215-
# Unset variables from .env.local so Bun can load fresh values
216-
if [ -f "$ENV_LOCAL_FILE" ]; then
217-
while IFS='=' read -r key _; do
218-
# Skip comments and empty lines
219-
case "$key" in
220-
'#'*|'') continue ;;
221-
esac
222-
# Remove any 'export ' prefix
223-
key="${key#export }"
224-
unset "$key" 2>/dev/null || true
225-
done < "$ENV_LOCAL_FILE"
226-
fi
227-
228-
# Unset variables from .env.development.local
229-
if [ -f "$ENV_DEVELOPMENT_LOCAL_FILE" ]; then
230-
while IFS='=' read -r key _; do
231-
case "$key" in
232-
'#'*|'') continue ;;
233-
esac
234-
key="${key#export }"
235-
unset "$key" 2>/dev/null || true
236-
done < "$ENV_DEVELOPMENT_LOCAL_FILE"
237-
fi
238-
}
239-
240205
run_bun() {
241206
create_env_symlinks
242207
check_env_setup
243-
clear_inherited_env_vars
244-
# Bun natively loads .env files in the correct precedence order
208+
# Bun natively loads .env files and gives them precedence over inherited shell vars
245209
exec "$REAL_BUN" "$@"
246210
}
247211

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import { describe, test, expect, afterEach } from 'bun:test'
2+
3+
import { getCliEnv, createTestCliEnv } from '../../utils/env'
4+
5+
describe('cli/utils/env', () => {
6+
describe('getCliEnv', () => {
7+
const originalEnv = { ...process.env }
8+
9+
afterEach(() => {
10+
// Restore original env
11+
Object.keys(process.env).forEach((key) => {
12+
if (!(key in originalEnv)) {
13+
delete process.env[key]
14+
}
15+
})
16+
Object.assign(process.env, originalEnv)
17+
})
18+
19+
test('returns current process.env values for base vars', () => {
20+
process.env.SHELL = '/bin/zsh'
21+
process.env.HOME = '/Users/testuser'
22+
const env = getCliEnv()
23+
expect(env.SHELL).toBe('/bin/zsh')
24+
expect(env.HOME).toBe('/Users/testuser')
25+
})
26+
27+
test('returns current process.env values for terminal detection vars', () => {
28+
process.env.TERM_PROGRAM = 'iTerm.app'
29+
process.env.KITTY_WINDOW_ID = '12345'
30+
const env = getCliEnv()
31+
expect(env.TERM_PROGRAM).toBe('iTerm.app')
32+
expect(env.KITTY_WINDOW_ID).toBe('12345')
33+
})
34+
35+
test('returns current process.env values for VS Code detection', () => {
36+
process.env.VSCODE_PID = '1234'
37+
process.env.VSCODE_THEME_KIND = 'dark'
38+
const env = getCliEnv()
39+
expect(env.VSCODE_PID).toBe('1234')
40+
expect(env.VSCODE_THEME_KIND).toBe('dark')
41+
})
42+
43+
test('returns current process.env values for Cursor detection', () => {
44+
process.env.CURSOR_PORT = '5678'
45+
process.env.CURSOR = 'true'
46+
const env = getCliEnv()
47+
expect(env.CURSOR_PORT).toBe('5678')
48+
expect(env.CURSOR).toBe('true')
49+
})
50+
51+
test('returns current process.env values for JetBrains detection', () => {
52+
process.env.TERMINAL_EMULATOR = 'JetBrains-JediTerm'
53+
process.env.IDE_CONFIG_DIR = '/path/to/idea'
54+
const env = getCliEnv()
55+
expect(env.TERMINAL_EMULATOR).toBe('JetBrains-JediTerm')
56+
expect(env.IDE_CONFIG_DIR).toBe('/path/to/idea')
57+
})
58+
59+
test('returns current process.env values for editor preferences', () => {
60+
process.env.EDITOR = 'vim'
61+
process.env.CODEBUFF_CLI_EDITOR = 'code'
62+
const env = getCliEnv()
63+
expect(env.EDITOR).toBe('vim')
64+
expect(env.CODEBUFF_CLI_EDITOR).toBe('code')
65+
})
66+
67+
test('returns current process.env values for theme preferences', () => {
68+
process.env.OPEN_TUI_THEME = 'dark'
69+
const env = getCliEnv()
70+
expect(env.OPEN_TUI_THEME).toBe('dark')
71+
})
72+
73+
test('returns current process.env values for binary build config', () => {
74+
process.env.CODEBUFF_IS_BINARY = 'true'
75+
process.env.CODEBUFF_CLI_VERSION = '1.0.0'
76+
const env = getCliEnv()
77+
expect(env.CODEBUFF_IS_BINARY).toBe('true')
78+
expect(env.CODEBUFF_CLI_VERSION).toBe('1.0.0')
79+
})
80+
81+
test('returns undefined for unset env vars', () => {
82+
delete process.env.KITTY_WINDOW_ID
83+
delete process.env.VSCODE_PID
84+
const env = getCliEnv()
85+
expect(env.KITTY_WINDOW_ID).toBeUndefined()
86+
expect(env.VSCODE_PID).toBeUndefined()
87+
})
88+
89+
test('returns a snapshot that does not change when process.env changes', () => {
90+
process.env.TERM_PROGRAM = 'iTerm.app'
91+
const env = getCliEnv()
92+
process.env.TERM_PROGRAM = 'vscode'
93+
expect(env.TERM_PROGRAM).toBe('iTerm.app')
94+
})
95+
})
96+
97+
describe('createTestCliEnv', () => {
98+
test('returns a CliEnv with default test values', () => {
99+
const env = createTestCliEnv()
100+
expect(env.HOME).toBe('/home/test')
101+
expect(env.NODE_ENV).toBe('test')
102+
expect(env.TERM).toBe('xterm-256color')
103+
expect(env.PATH).toBe('/usr/bin')
104+
})
105+
106+
test('returns undefined for CLI-specific vars by default', () => {
107+
const env = createTestCliEnv()
108+
expect(env.KITTY_WINDOW_ID).toBeUndefined()
109+
expect(env.VSCODE_PID).toBeUndefined()
110+
expect(env.CURSOR_PORT).toBeUndefined()
111+
expect(env.IDE_CONFIG_DIR).toBeUndefined()
112+
expect(env.CODEBUFF_IS_BINARY).toBeUndefined()
113+
})
114+
115+
test('allows overriding terminal detection vars', () => {
116+
const env = createTestCliEnv({
117+
TERM_PROGRAM: 'iTerm.app',
118+
KITTY_WINDOW_ID: '12345',
119+
SIXEL_SUPPORT: 'true',
120+
})
121+
expect(env.TERM_PROGRAM).toBe('iTerm.app')
122+
expect(env.KITTY_WINDOW_ID).toBe('12345')
123+
expect(env.SIXEL_SUPPORT).toBe('true')
124+
})
125+
126+
test('allows overriding VS Code detection vars', () => {
127+
const env = createTestCliEnv({
128+
VSCODE_PID: '1234',
129+
VSCODE_THEME_KIND: 'dark',
130+
VSCODE_GIT_IPC_HANDLE: '/tmp/vscode-git',
131+
})
132+
expect(env.VSCODE_PID).toBe('1234')
133+
expect(env.VSCODE_THEME_KIND).toBe('dark')
134+
expect(env.VSCODE_GIT_IPC_HANDLE).toBe('/tmp/vscode-git')
135+
})
136+
137+
test('allows overriding editor preferences', () => {
138+
const env = createTestCliEnv({
139+
EDITOR: 'vim',
140+
VISUAL: 'code',
141+
CODEBUFF_CLI_EDITOR: 'cursor',
142+
})
143+
expect(env.EDITOR).toBe('vim')
144+
expect(env.VISUAL).toBe('code')
145+
expect(env.CODEBUFF_CLI_EDITOR).toBe('cursor')
146+
})
147+
148+
test('allows overriding binary build config', () => {
149+
const env = createTestCliEnv({
150+
CODEBUFF_IS_BINARY: 'true',
151+
CODEBUFF_CLI_VERSION: '2.0.0',
152+
CODEBUFF_CLI_TARGET: 'darwin-arm64',
153+
})
154+
expect(env.CODEBUFF_IS_BINARY).toBe('true')
155+
expect(env.CODEBUFF_CLI_VERSION).toBe('2.0.0')
156+
expect(env.CODEBUFF_CLI_TARGET).toBe('darwin-arm64')
157+
})
158+
159+
test('allows overriding default values', () => {
160+
const env = createTestCliEnv({
161+
HOME: '/custom/home',
162+
NODE_ENV: 'production',
163+
})
164+
expect(env.HOME).toBe('/custom/home')
165+
expect(env.NODE_ENV).toBe('production')
166+
})
167+
})
168+
})

cli/src/components/message-block.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import React, {
99

1010
import { AgentBranchItem } from './agent-branch-item'
1111
import { Button } from './button'
12+
import { CopyIconButton } from './copy-icon-button'
1213
import { ImageCard } from './image-card'
1314
import { MessageFooter } from './message-footer'
1415
import { ValidationErrorPopover } from './validation-error-popover'
@@ -188,7 +189,7 @@ export const MessageBlock: React.FC<MessageBlockProps> = ({
188189
width: '100%',
189190
}}
190191
>
191-
{/* User message timestamp with error indicator button (non-bash commands) */}
192+
{/* User message timestamp with copy button and error indicator (non-bash commands) */}
192193
{isUser && !bashCwd && (
193194
<box style={{ flexDirection: 'row', alignItems: 'center', gap: 1 }}>
194195
<text
@@ -201,6 +202,8 @@ export const MessageBlock: React.FC<MessageBlockProps> = ({
201202
{`[${timestamp}]`}
202203
</text>
203204

205+
<CopyIconButton textToCopy={content} />
206+
204207
{validationErrors && validationErrors.length > 0 && (
205208
<Button
206209
onClick={() => setShowValidationPopover(!showValidationPopover)}
@@ -218,7 +221,7 @@ export const MessageBlock: React.FC<MessageBlockProps> = ({
218221
</box>
219222
)}
220223

221-
{/* Bash command metadata header (timestamp + cwd) - now for user messages with bashCwd */}
224+
{/* Bash command metadata header (timestamp + copy button + cwd) */}
222225
{bashCwd && (
223226
<box style={{ flexDirection: 'row', alignItems: 'center', gap: 1 }}>
224227
<text
@@ -230,6 +233,7 @@ export const MessageBlock: React.FC<MessageBlockProps> = ({
230233
>
231234
{`[${timestamp}]`}
232235
</text>
236+
<CopyIconButton textToCopy={content} />
233237
<text
234238
attributes={TextAttributes.DIM}
235239
style={{

cli/src/hooks/use-usage-query.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import { env } from '@codebuff/common/env'
12
import { useQuery, useQueryClient } from '@tanstack/react-query'
23

34
import { getAuthToken } from '../utils/auth'
45
import { logger as defaultLogger } from '../utils/logger'
56

7+
import type { ClientEnv } from '@codebuff/common/types/contracts/env'
68
import type { Logger } from '@codebuff/common/types/contracts/logger'
79

810
// Query keys for type-safe cache management
@@ -26,6 +28,7 @@ interface UsageResponse {
2628
interface FetchUsageParams {
2729
authToken: string
2830
logger?: Logger
31+
clientEnv?: ClientEnv
2932
}
3033

3134
/**
@@ -34,8 +37,9 @@ interface FetchUsageParams {
3437
export async function fetchUsageData({
3538
authToken,
3639
logger = defaultLogger,
40+
clientEnv = env,
3741
}: FetchUsageParams): Promise<UsageResponse> {
38-
const appUrl = process.env.NEXT_PUBLIC_CODEBUFF_APP_URL
42+
const appUrl = clientEnv.NEXT_PUBLIC_CODEBUFF_APP_URL
3943
if (!appUrl) {
4044
throw new Error('NEXT_PUBLIC_CODEBUFF_APP_URL is not set')
4145
}

cli/src/hooks/use-why-did-you-update.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { useEffect, useRef } from 'react'
22

3+
import { getCliEnv } from '../utils/env'
4+
35
import { logger } from '../utils/logger'
46

57
/**
@@ -35,9 +37,10 @@ export function useWhyDidYouUpdate<T extends Record<string, any>>(
3537
enabled?: boolean
3638
} = {},
3739
): void {
40+
const env = getCliEnv()
3841
const {
3942
logLevel = 'info',
40-
enabled = process.env.NODE_ENV === 'development',
43+
enabled = env.NODE_ENV === 'development',
4144
} = options
4245

4346
const previousProps = useRef<T | null>(null)
@@ -115,7 +118,8 @@ export function useWhyDidYouUpdateById<T extends Record<string, any>>(
115118
enabled?: boolean
116119
} = {},
117120
): void {
118-
const { logLevel = 'info', enabled = process.env.ENVIRONMENT === 'dev' } =
121+
const env = getCliEnv()
122+
const { logLevel = 'info', enabled = env.NODE_ENV === 'development' } =
119123
options
120124

121125
const previousProps = useRef<T | null>(null)

cli/src/native/ripgrep.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import path from 'path'
22

3+
import { getCliEnv } from '../utils/env'
34
import { getBundledRgPath } from '@codebuff/sdk'
45
import { spawnSync } from 'bun'
56

67
import { logger } from '../utils/logger'
78

89
const getRipgrepPath = async (): Promise<string> => {
10+
const env = getCliEnv()
911
// In dev mode, use the SDK's bundled ripgrep binary
10-
if (!process.env.CODEBUFF_IS_BINARY) {
12+
if (!env.CODEBUFF_IS_BINARY) {
1113
return getBundledRgPath()
1214
}
1315

0 commit comments

Comments
 (0)