Skip to content

Commit 7fc1684

Browse files
committed
feat(env): enforce package boundaries + test defaults
Typecheck-time enforcement (scripts/check-env-architecture.ts): - Ban common/src/** from importing @codebuff/internal/* (layering/secrets) - Ban 'use client' modules from importing @codebuff/internal/env - Enforce CLI/SDK process.env access through designated env helpers Runtime guards (@codebuff/internal/env conditional exports): - browser/react-server conditions route to 'server-only' stubs - Provides defense-in-depth for Next.js bundle boundaries Other changes: - Route CLI/SDK process.env access through env helpers - Preload safe NEXT_PUBLIC_* defaults in test setup - Clarify test-only agent runtime fixtures
1 parent 2a263bd commit 7fc1684

30 files changed

+987
-825
lines changed

bunfig.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ linkWorkspacePackages = true
77
[test]
88
# Exclude test repositories from test execution to prevent timeouts
99
exclude = ["evals/test-repos/**"]
10+
preload = ["./sdk/test/setup-env.ts"]

cli/src/commands/router.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { runTerminalCommand } from '@codebuff/sdk'
22

3+
34
import {
45
findCommand,
56
type RouterParams,
@@ -25,10 +26,9 @@ import {
2526
createRunTerminalToolResult,
2627
} from '../utils/bash-messages'
2728
import { showClipboardMessage } from '../utils/clipboard'
29+
import { getSystemProcessEnv } from '../utils/env'
2830
import { getSystemMessage, getUserMessage } from '../utils/message-history'
2931

30-
31-
3232
/**
3333
* Run a bash command with automatic ghost/direct mode selection.
3434
* Uses ghost mode when streaming or chain in progress, otherwise adds directly to chat history.
@@ -74,7 +74,7 @@ export function runBashCommand(command: string) {
7474
process_type: 'SYNC',
7575
cwd: commandCwd,
7676
timeout_seconds: -1,
77-
env: process.env,
77+
env: getSystemProcessEnv(),
7878
})
7979
.then(([{ value }]) => {
8080
const stdout = 'stdout' in value ? value.stdout || '' : ''

cli/src/hooks/use-theme.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import { create } from 'zustand'
88

99
import { themeConfig, buildTheme } from '../utils/theme-config'
10+
import { getCliEnv } from '../utils/env'
1011
import {
1112
chatThemes,
1213
cloneChatTheme,
@@ -58,7 +59,8 @@ const THEME_PRIORITY: ThemeDetector[] = [
5859
]
5960

6061
export const detectSystemTheme = (): ThemeName => {
61-
const envPreference = process.env.OPEN_TUI_THEME ?? process.env.OPENTUI_THEME
62+
const env = getCliEnv()
63+
const envPreference = env.OPEN_TUI_THEME ?? env.OPENTUI_THEME
6264
const normalizedEnv = envPreference?.toLowerCase()
6365

6466
if (normalizedEnv === 'dark' || normalizedEnv === 'light') {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { logger } from '../utils/logger'
2424
* function MyComponent(props: MyProps) {
2525
* useWhyDidYouUpdate('MyComponent', props, {
2626
* logLevel: 'debug',
27-
* enabled: process.env.NODE_ENV === 'development'
27+
* enabled: getCliEnv().NODE_ENV === 'development'
2828
* })
2929
* return <div>...</div>
3030
* }

cli/src/types/env.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ export type CliEnv = BaseEnv & {
2020
KITTY_WINDOW_ID?: string
2121
SIXEL_SUPPORT?: string
2222
ZED_NODE_ENV?: string
23+
ZED_TERM?: string
24+
ZED_SHELL?: string
2325

2426
// VS Code family detection
2527
VSCODE_THEME_KIND?: string
@@ -54,6 +56,7 @@ export type CliEnv = BaseEnv & {
5456
CODEBUFF_CLI_VERSION?: string
5557
CODEBUFF_CLI_TARGET?: string
5658
CODEBUFF_RG_PATH?: string
59+
CODEBUFF_SCROLL_MULTIPLIER?: string
5760
}
5861

5962
/**

cli/src/utils/chat-scroll-accel.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { Queue } from './arrays'
22
import { clamp } from './math'
3+
import { getCliEnv } from './env'
34

45
import type { ScrollAcceleration } from '@opentui/core'
5-
6-
const SCROLL_MULTIPLIER = 'CODEBUFF_SCROLL_MULTIPLIER'
6+
import type { CliEnv } from '../types/env'
77

88
const ENVIRONMENT_TYPE_VARS = [
99
'TERM_PROGRAM',
@@ -30,18 +30,20 @@ type ScrollEnvironment = {
3030
multiplier: number
3131
}
3232

33-
const resolveScrollEnvironment = (): ScrollEnvironment => {
34-
let multiplier = parseFloat(process.env[SCROLL_MULTIPLIER] ?? '')
33+
const resolveScrollEnvironment = (
34+
env: CliEnv = getCliEnv(),
35+
): ScrollEnvironment => {
36+
let multiplier = parseFloat(env.CODEBUFF_SCROLL_MULTIPLIER ?? '')
3537

3638
if (Number.isNaN(multiplier)) {
3739
multiplier = 1
3840
}
3941

4042
for (const hintVar of ENVIRONMENT_TYPE_VARS) {
41-
const value = process.env[hintVar]
42-
for (const env of ENVIRONMENTS) {
43-
if (value?.includes(env)) {
44-
return { type: env, multiplier }
43+
const value = env[hintVar]
44+
for (const environment of ENVIRONMENTS) {
45+
if (value?.includes(environment)) {
46+
return { type: environment, multiplier }
4547
}
4648
}
4749
}

cli/src/utils/codebuff-client.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { getCliEnv } from './env'
21
import { API_KEY_ENV_VAR } from '@codebuff/common/old-constants'
32
import { AskUserBridge } from '@codebuff/common/utils/ask-user-bridge'
43
import { CodebuffClient } from '@codebuff/sdk'
54

65
import { getAuthTokenDetails } from './auth'
6+
import { getCliEnv, getSystemProcessEnv } from './env'
77
import { loadAgentDefinitions } from './local-agent-registry'
88
import { logger } from './logger'
99
import { getRgPath } from '../native/ripgrep'
@@ -64,7 +64,7 @@ export async function getCodebuffClient(): Promise<CodebuffClient | null> {
6464
try {
6565
const rgPath = await getRgPath()
6666
// Note: We still set process.env here because SDK reads from it
67-
process.env.CODEBUFF_RG_PATH = rgPath
67+
getSystemProcessEnv().CODEBUFF_RG_PATH = rgPath
6868
} catch (error) {
6969
logger.error(error, 'Failed to set up ripgrep binary for SDK')
7070
}

cli/src/utils/env.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@
55
* process env with CLI-specific vars for terminal/IDE detection.
66
*/
77

8-
import {
9-
getBaseEnv,
10-
createTestBaseEnv,
11-
} from '@codebuff/common/env-process'
8+
import { getBaseEnv, createTestBaseEnv } from '@codebuff/common/env-process'
129

1310
import type { CliEnv } from '../types/env'
1411

@@ -23,6 +20,8 @@ export const getCliEnv = (): CliEnv => ({
2320
KITTY_WINDOW_ID: process.env.KITTY_WINDOW_ID,
2421
SIXEL_SUPPORT: process.env.SIXEL_SUPPORT,
2522
ZED_NODE_ENV: process.env.ZED_NODE_ENV,
23+
ZED_TERM: process.env.ZED_TERM,
24+
ZED_SHELL: process.env.ZED_SHELL,
2625

2726
// VS Code family detection
2827
VSCODE_THEME_KIND: process.env.VSCODE_THEME_KIND,
@@ -57,21 +56,29 @@ export const getCliEnv = (): CliEnv => ({
5756
CODEBUFF_CLI_VERSION: process.env.CODEBUFF_CLI_VERSION,
5857
CODEBUFF_CLI_TARGET: process.env.CODEBUFF_CLI_TARGET,
5958
CODEBUFF_RG_PATH: process.env.CODEBUFF_RG_PATH,
59+
CODEBUFF_SCROLL_MULTIPLIER: process.env.CODEBUFF_SCROLL_MULTIPLIER,
6060
})
6161

62+
/**
63+
* Get the raw system process.env object.
64+
* Use this when you need to pass the full environment to subprocesses
65+
* or when you need to set environment variables at runtime.
66+
*/
67+
export const getSystemProcessEnv = (): NodeJS.ProcessEnv => process.env
68+
6269
/**
6370
* Create a test CliEnv with optional overrides.
6471
* Composes from createTestBaseEnv() for DRY.
6572
*/
66-
export const createTestCliEnv = (
67-
overrides: Partial<CliEnv> = {},
68-
): CliEnv => ({
73+
export const createTestCliEnv = (overrides: Partial<CliEnv> = {}): CliEnv => ({
6974
...createTestBaseEnv(),
7075

7176
// CLI-specific defaults
7277
KITTY_WINDOW_ID: undefined,
7378
SIXEL_SUPPORT: undefined,
7479
ZED_NODE_ENV: undefined,
80+
ZED_TERM: undefined,
81+
ZED_SHELL: undefined,
7582
VSCODE_THEME_KIND: undefined,
7683
VSCODE_COLOR_THEME_KIND: undefined,
7784
VSCODE_GIT_IPC_HANDLE: undefined,
@@ -94,5 +101,6 @@ export const createTestCliEnv = (
94101
CODEBUFF_CLI_VERSION: undefined,
95102
CODEBUFF_CLI_TARGET: undefined,
96103
CODEBUFF_RG_PATH: undefined,
104+
CODEBUFF_SCROLL_MULTIPLIER: undefined,
97105
...overrides,
98106
})

common/src/__tests__/agent-validation.test.ts

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -74,39 +74,6 @@ describe('Agent Validation', () => {
7474
expect(result.templates.brainstormer.id).toBe('brainstormer')
7575
})
7676

77-
test.skip('should validate spawnable agents', async () => {
78-
const fileContext: ProjectFileContext = {
79-
...mockFileContext,
80-
agentTemplates: {
81-
'invalid.ts': {
82-
id: 'invalid_agent',
83-
version: '1.0.0',
84-
displayName: 'Invalid',
85-
spawnerPrompt: 'Invalid agent',
86-
model: 'anthropic/claude-4-sonnet-20250522',
87-
systemPrompt: 'Test',
88-
instructionsPrompt: 'Test',
89-
stepPrompt: 'Test',
90-
spawnableAgents: ['nonexistent_agent'],
91-
outputMode: 'last_message',
92-
includeMessageHistory: true,
93-
inheritParentSystemPrompt: false,
94-
toolNames: ['end_turn'],
95-
},
96-
},
97-
}
98-
99-
const result = validateAgents({
100-
agentTemplates: fileContext.agentTemplates || {},
101-
logger,
102-
})
103-
104-
expect(result.validationErrors).toHaveLength(1)
105-
expect(result.validationErrors[0].message).toContain(
106-
'Invalid spawnable agents: nonexistent_agent',
107-
)
108-
})
109-
11077
it('should merge static and dynamic templates', async () => {
11178
const fileContext: ProjectFileContext = {
11279
...mockFileContext,

common/src/env.ts

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,49 @@
1-
import { clientEnvSchema, clientProcessEnv } from './env-schema'
1+
import {
2+
clientEnvSchema,
3+
clientProcessEnv,
4+
type ClientInput,
5+
} from './env-schema'
26

3-
// Only log environment in non-production
4-
if (process.env.NEXT_PUBLIC_CB_ENVIRONMENT !== 'prod') {
5-
console.log('Using environment:', process.env.NEXT_PUBLIC_CB_ENVIRONMENT)
7+
const isTestRuntime =
8+
process.env.NODE_ENV === 'test' || process.env.BUN_ENV === 'test'
9+
10+
const TEST_ENV_DEFAULTS: ClientInput = {
11+
NEXT_PUBLIC_CB_ENVIRONMENT: 'test',
12+
NEXT_PUBLIC_CODEBUFF_APP_URL: 'http://localhost:3000',
13+
NEXT_PUBLIC_SUPPORT_EMAIL: 'support@codebuff.com',
14+
NEXT_PUBLIC_POSTHOG_API_KEY: 'test-posthog-key',
15+
NEXT_PUBLIC_POSTHOG_HOST_URL: 'https://us.i.posthog.com',
16+
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: 'pk_test_placeholder',
17+
NEXT_PUBLIC_STRIPE_CUSTOMER_PORTAL:
18+
'https://billing.stripe.com/p/login/test_placeholder',
19+
NEXT_PUBLIC_GOOGLE_SITE_VERIFICATION_ID: 'test-verification',
20+
NEXT_PUBLIC_WEB_PORT: '3000',
21+
}
22+
23+
const envInput = isTestRuntime
24+
? { ...TEST_ENV_DEFAULTS, ...clientProcessEnv }
25+
: clientProcessEnv
26+
27+
const parsedEnv = clientEnvSchema.safeParse(envInput)
28+
if (!parsedEnv.success) {
29+
throw parsedEnv.error
630
}
731

8-
export const env = clientEnvSchema.parse(clientProcessEnv)
32+
export const env = parsedEnv.data
33+
34+
// Populate process.env with defaults during tests so direct access works
35+
if (isTestRuntime) {
36+
for (const [key, value] of Object.entries(TEST_ENV_DEFAULTS)) {
37+
if (!process.env[key] && typeof value === 'string') {
38+
process.env[key] = value
39+
}
40+
}
41+
}
42+
43+
// Only log environment in non-production
44+
if (env.NEXT_PUBLIC_CB_ENVIRONMENT !== 'prod') {
45+
console.log('Using environment:', env.NEXT_PUBLIC_CB_ENVIRONMENT)
46+
}
947

1048
// Derived environment constants for convenience
1149
export const IS_DEV = env.NEXT_PUBLIC_CB_ENVIRONMENT === 'dev'

0 commit comments

Comments
 (0)