Skip to content

Commit 1b4f499

Browse files
committed
Merge branch 'main' into openai-native
2 parents 4a365d4 + 57dc84c commit 1b4f499

33 files changed

+3438
-252
lines changed

cli/src/components/agent-mode-toggle.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,10 @@ export const AgentModeToggle = ({
214214
}}
215215
onMouseOut={handleMouseOut}
216216
>
217-
<text wrapMode="none">
217+
<text
218+
wrapMode="none"
219+
fg={isCollapsedHovered ? theme.foreground : theme.muted}
220+
>
218221
{isCollapsedHovered ? (
219222
<b>{`< ${MODE_LABELS[mode]}`}</b>
220223
) : (

cli/src/index.tsx

Lines changed: 131 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,24 @@
11
#!/usr/bin/env node
2+
3+
const cliEntryPoint =
4+
(typeof Bun !== 'undefined' && typeof Bun.main === 'string' && Bun.main) ||
5+
(typeof process !== 'undefined' &&
6+
Array.isArray(process.argv) &&
7+
process.argv[1]) ||
8+
''
9+
10+
if (cliEntryPoint && typeof globalThis !== 'undefined') {
11+
const globalScope = globalThis as Record<string, unknown>
12+
if (!('__CLI_ENTRY_POINT' in globalScope)) {
13+
Object.defineProperty(globalScope, '__CLI_ENTRY_POINT', {
14+
value: cliEntryPoint,
15+
enumerable: false,
16+
writable: false,
17+
configurable: false,
18+
})
19+
}
20+
}
21+
222
import './polyfills/bun-strip-ansi'
323
import { createRequire } from 'module'
424

@@ -18,6 +38,40 @@ import { initializeThemeStore } from './state/theme-store'
1838

1939
const require = createRequire(import.meta.url)
2040

41+
const INTERNAL_OSC_FLAG = '--internal-osc-detect'
42+
43+
function isOscDetectionRun(): boolean {
44+
return process.argv.includes(INTERNAL_OSC_FLAG)
45+
}
46+
47+
async function runOscDetectionSubprocess(): Promise<void> {
48+
// Set env vars to keep subprocess quiet
49+
process.env.__INTERNAL_OSC_DETECT = '1'
50+
process.env.CODEBUFF_GITHUB_ACTIONS = 'true'
51+
52+
// Avoid importing logger or other modules that produce output
53+
const { detectTerminalTheme, terminalSupportsOSC } = await import(
54+
'./utils/terminal-color-detection'
55+
)
56+
57+
if (!terminalSupportsOSC()) {
58+
console.log(JSON.stringify({ theme: null }))
59+
await new Promise((resolve) => setImmediate(resolve))
60+
process.exit(0)
61+
}
62+
63+
try {
64+
const theme = await detectTerminalTheme()
65+
console.log(JSON.stringify({ theme }))
66+
await new Promise((resolve) => setImmediate(resolve))
67+
} catch {
68+
console.log(JSON.stringify({ theme: null }))
69+
await new Promise((resolve) => setImmediate(resolve))
70+
}
71+
72+
process.exit(0)
73+
}
74+
2175
function loadPackageVersion(): string {
2276
if (process.env.CODEBUFF_CLI_VERSION) {
2377
return process.env.CODEBUFF_CLI_VERSION
@@ -37,6 +91,24 @@ function loadPackageVersion(): string {
3791

3892
const VERSION = loadPackageVersion()
3993

94+
function createQueryClient(): QueryClient {
95+
return new QueryClient({
96+
defaultOptions: {
97+
queries: {
98+
staleTime: 5 * 60 * 1000, // 5 minutes - auth tokens don't change frequently
99+
gcTime: 10 * 60 * 1000, // 10 minutes - keep cached data a bit longer
100+
retry: false, // Don't retry failed auth queries automatically
101+
refetchOnWindowFocus: false, // CLI doesn't have window focus
102+
refetchOnReconnect: true, // Refetch when network reconnects
103+
refetchOnMount: false, // Don't refetch on every mount
104+
},
105+
mutations: {
106+
retry: 1, // Retry mutations once on failure
107+
},
108+
},
109+
})
110+
}
111+
40112
type ParsedArgs = {
41113
initialPrompt: string | null
42114
agent?: string
@@ -70,85 +142,63 @@ function parseArgs(): ParsedArgs {
70142
}
71143
}
72144

73-
const { initialPrompt, agent, clearLogs } = parseArgs()
145+
async function bootstrapCli(): Promise<void> {
146+
const { initialPrompt, agent, clearLogs } = parseArgs()
74147

75-
// Initialize theme store and watchers
76-
initializeThemeStore()
148+
initializeThemeStore()
77149

78-
if (clearLogs) {
79-
clearLogFile()
80-
}
81-
82-
const loadedAgentsData = getLoadedAgentsData()
150+
if (clearLogs) {
151+
clearLogFile()
152+
}
83153

84-
// Validate local agents and capture any errors
85-
let validationErrors: Array<{ id: string; message: string }> = []
86-
if (loadedAgentsData) {
87-
const agentDefinitions = loadAgentDefinitions()
88-
const validationResult = await validateAgents(agentDefinitions, {
89-
remote: true, // Use remote validation to ensure spawnable agents exist
90-
})
154+
const loadedAgentsData = getLoadedAgentsData()
91155

92-
if (!validationResult.success) {
93-
validationErrors = validationResult.validationErrors
94-
}
95-
}
156+
let validationErrors: Array<{ id: string; message: string }> = []
157+
if (loadedAgentsData) {
158+
const agentDefinitions = loadAgentDefinitions()
159+
const validationResult = await validateAgents(agentDefinitions, {
160+
remote: true,
161+
})
96162

97-
// Create QueryClient instance with CLI-optimized defaults
98-
const queryClient = new QueryClient({
99-
defaultOptions: {
100-
queries: {
101-
staleTime: 5 * 60 * 1000, // 5 minutes - auth tokens don't change frequently
102-
gcTime: 10 * 60 * 1000, // 10 minutes - keep cached data a bit longer
103-
retry: false, // Don't retry failed auth queries automatically
104-
refetchOnWindowFocus: false, // CLI doesn't have window focus
105-
refetchOnReconnect: true, // Refetch when network reconnects
106-
refetchOnMount: false, // Don't refetch on every mount
107-
},
108-
mutations: {
109-
retry: 1, // Retry mutations once on failure
110-
},
111-
},
112-
})
113-
114-
// Wrapper component to handle async auth check
115-
const AppWithAsyncAuth = () => {
116-
const [requireAuth, setRequireAuth] = React.useState<boolean | null>(null)
117-
const [hasInvalidCredentials, setHasInvalidCredentials] =
118-
React.useState(false)
119-
120-
React.useEffect(() => {
121-
// Check authentication asynchronously
122-
const userCredentials = getUserCredentials()
123-
const apiKey =
124-
userCredentials?.authToken || process.env[API_KEY_ENV_VAR] || ''
125-
126-
if (!apiKey) {
127-
// No credentials, require auth
128-
setRequireAuth(true)
129-
setHasInvalidCredentials(false)
130-
return
163+
if (!validationResult.success) {
164+
validationErrors = validationResult.validationErrors
131165
}
166+
}
132167

133-
// We have credentials - require auth but show invalid credentials banner until validation succeeds
134-
setHasInvalidCredentials(true)
135-
setRequireAuth(false)
136-
}, [])
137-
138-
return (
139-
<App
140-
initialPrompt={initialPrompt}
141-
agentId={agent}
142-
requireAuth={requireAuth}
143-
hasInvalidCredentials={hasInvalidCredentials}
144-
loadedAgentsData={loadedAgentsData}
145-
validationErrors={validationErrors}
146-
/>
147-
)
148-
}
168+
const queryClient = createQueryClient()
169+
170+
const AppWithAsyncAuth = () => {
171+
const [requireAuth, setRequireAuth] = React.useState<boolean | null>(null)
172+
const [hasInvalidCredentials, setHasInvalidCredentials] =
173+
React.useState(false)
174+
175+
React.useEffect(() => {
176+
const userCredentials = getUserCredentials()
177+
const apiKey =
178+
userCredentials?.authToken || process.env[API_KEY_ENV_VAR] || ''
179+
180+
if (!apiKey) {
181+
setRequireAuth(true)
182+
setHasInvalidCredentials(false)
183+
return
184+
}
185+
186+
setHasInvalidCredentials(true)
187+
setRequireAuth(false)
188+
}, [])
189+
190+
return (
191+
<App
192+
initialPrompt={initialPrompt}
193+
agentId={agent}
194+
requireAuth={requireAuth}
195+
hasInvalidCredentials={hasInvalidCredentials}
196+
loadedAgentsData={loadedAgentsData}
197+
validationErrors={validationErrors}
198+
/>
199+
)
200+
}
149201

150-
// Start app immediately with QueryClientProvider
151-
function startApp() {
152202
render(
153203
<QueryClientProvider client={queryClient}>
154204
<AppWithAsyncAuth />
@@ -160,4 +210,13 @@ function startApp() {
160210
)
161211
}
162212

163-
startApp()
213+
async function main(): Promise<void> {
214+
if (isOscDetectionRun()) {
215+
await runOscDetectionSubprocess()
216+
return
217+
}
218+
219+
await bootstrapCli()
220+
}
221+
222+
void main()

cli/src/utils/detect-shell.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { execSync } from 'child_process'
2+
3+
type KnownShell =
4+
| 'bash'
5+
| 'zsh'
6+
| 'fish'
7+
| 'cmd.exe'
8+
| 'powershell'
9+
| 'unknown'
10+
11+
type ShellName = KnownShell | string
12+
13+
let cachedShell: ShellName | null = null
14+
15+
const SHELL_ALIASES: Record<string, KnownShell> = {
16+
bash: 'bash',
17+
zsh: 'zsh',
18+
fish: 'fish',
19+
cmd: 'cmd.exe',
20+
'cmd.exe': 'cmd.exe',
21+
pwsh: 'powershell',
22+
powershell: 'powershell',
23+
'powershell.exe': 'powershell',
24+
}
25+
26+
export function detectShell(): ShellName {
27+
if (cachedShell) {
28+
return cachedShell
29+
}
30+
31+
const detected =
32+
detectFromEnvironment() ?? detectViaParentProcessInspection() ?? 'unknown'
33+
cachedShell = detected
34+
return detected
35+
}
36+
37+
function detectFromEnvironment(): ShellName | null {
38+
const candidates: Array<string | undefined> = []
39+
40+
if (process.platform === 'win32') {
41+
candidates.push(process.env.COMSPEC, process.env.SHELL)
42+
} else {
43+
candidates.push(process.env.SHELL)
44+
}
45+
46+
for (const candidate of candidates) {
47+
const normalized = normalizeCandidate(candidate)
48+
if (normalized) {
49+
return normalized
50+
}
51+
}
52+
53+
return null
54+
}
55+
56+
function detectViaParentProcessInspection(): ShellName | null {
57+
try {
58+
if (process.platform === 'win32') {
59+
const parentProcess = execSync(
60+
'wmic process get ParentProcessId,CommandLine',
61+
{ stdio: 'pipe' },
62+
)
63+
.toString()
64+
.toLowerCase()
65+
66+
if (parentProcess.includes('powershell')) return 'powershell'
67+
if (parentProcess.includes('cmd.exe')) return 'cmd.exe'
68+
} else {
69+
const parentProcess = execSync(`ps -p ${process.ppid} -o comm=`, {
70+
stdio: 'pipe',
71+
})
72+
.toString()
73+
.trim()
74+
const normalized = normalizeCandidate(parentProcess)
75+
if (normalized) return normalized
76+
}
77+
} catch {
78+
// Ignore inspection errors
79+
}
80+
81+
return null
82+
}
83+
84+
function normalizeCandidate(value?: string | null): ShellName | null {
85+
if (!value) {
86+
return null
87+
}
88+
89+
const trimmed = value.trim()
90+
if (!trimmed) {
91+
return null
92+
}
93+
94+
const lower = trimmed.toLowerCase()
95+
const parts = lower.split(/[/\\]/)
96+
const last = parts.pop() ?? lower
97+
const base = last.endsWith('.exe') ? last.slice(0, -4) : last
98+
99+
if (SHELL_ALIASES[base]) {
100+
return SHELL_ALIASES[base]
101+
}
102+
103+
if (SHELL_ALIASES[last]) {
104+
return SHELL_ALIASES[last]
105+
}
106+
107+
if (base.endsWith('sh')) {
108+
return base
109+
}
110+
111+
return null
112+
}

0 commit comments

Comments
 (0)