Skip to content

Commit e0cde1b

Browse files
agent-relay-code[bot]khaliqgant
authored andcommitted
chore: apply pr-reviewer fixes for #143
1 parent 3c775e8 commit e0cde1b

7 files changed

Lines changed: 87 additions & 7 deletions

File tree

src/main/broker.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ vi.mock('./burn', () => ({
192192

193193
import {
194194
BrokerManager,
195+
isCommandAvailableWithAugmentedPath,
195196
parseBrokerInitCliFlags,
196197
resolveAgentRelayMcpCommand,
197198
resolveBundledBrokerBinary
@@ -209,6 +210,7 @@ const originalPlatformDescriptor = Object.getOwnPropertyDescriptor(process, 'pla
209210
const originalPublicEnv = process.env.PUBLIC
210211
const originalProgramDataEnv = process.env.ProgramData
211212
const originalPersonaHarnessReadyTimeoutEnv = process.env.PEAR_PERSONA_HARNESS_READY_TIMEOUT_MS
213+
const originalPathEnv = process.env.PATH
212214

213215
function setProcessPlatform(platform: NodeJS.Platform): void {
214216
Object.defineProperty(process, 'platform', {
@@ -2342,3 +2344,28 @@ describe('BrokerManager spawnAgent CLI preflight', () => {
23422344
await manager.shutdown()
23432345
})
23442346
})
2347+
2348+
describe('isCommandAvailableWithAugmentedPath', () => {
2349+
let tempDir: string | null = null
2350+
2351+
afterEach(async () => {
2352+
process.env.PATH = originalPathEnv
2353+
if (tempDir) await rm(tempDir, { recursive: true, force: true })
2354+
tempDir = null
2355+
})
2356+
2357+
it('resolves commands without mutating PATH', async () => {
2358+
tempDir = await mkdtemp(join(tmpdir(), 'pear-cli-path-'))
2359+
const toolPath = join(tempDir, 'opencode')
2360+
await writeFile(toolPath, '#!/bin/sh\nexit 0\n')
2361+
await chmod(toolPath, 0o755)
2362+
process.env.PATH = tempDir
2363+
2364+
expect(isCommandAvailableWithAugmentedPath('opencode')).toBe(true)
2365+
expect(process.env.PATH).toBe(tempDir)
2366+
})
2367+
2368+
it('returns false for blank commands', () => {
2369+
expect(isCommandAvailableWithAugmentedPath(' ')).toBe(false)
2370+
})
2371+
})

src/main/broker.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,15 @@ export function resolveCommandWithAugmentedPath(command: string): string | undef
114114
return undefined
115115
}
116116

117+
export function isCommandAvailableWithAugmentedPath(command: string): boolean {
118+
const trimmed = command.trim()
119+
if (!trimmed) return false
120+
return Boolean(resolveCommandOnPath(trimmed, {
121+
...process.env,
122+
PATH: augmentedPath()
123+
}))
124+
}
125+
117126
function executableCliPath(input: SpawnPtyInput): string {
118127
return isAbsolute(input.cli)
119128
? input.cli

src/main/ipc-handlers.test.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ const mock = vi.hoisted(() => {
4747
detachCloudSandbox: vi.fn(),
4848
onBrokerEvent: vi.fn()
4949
},
50+
isCommandAvailableWithAugmentedPath: vi.fn(),
5051
integrationsManager: {
5152
notifyAgentState: vi.fn(async () => undefined),
5253
initialSpawnInstructions: vi.fn(),
@@ -105,7 +106,8 @@ vi.mock('./store', () => ({
105106
}))
106107

107108
vi.mock('./broker', () => ({
108-
brokerManager: mock.brokerManager
109+
brokerManager: mock.brokerManager,
110+
isCommandAvailableWithAugmentedPath: mock.isCommandAvailableWithAugmentedPath
109111
}))
110112

111113
vi.mock('./git', () => ({
@@ -208,10 +210,14 @@ describe('registerIpcHandlers broker:spawn-agent', () => {
208210
})
209211

210212
describe('registerIpcHandlers broker:list-personas', () => {
213+
=======
214+
describe('registerIpcHandlers broker:check-cli-available', () => {
215+
>>>>>>> ed6285c (chore: apply pr-reviewer fixes for #143)
211216
beforeEach(() => {
212217
mock.handlers.clear()
213218
mock.ipcMain.handle.mockClear()
214219
mock.ipcMain.on.mockClear()
220+
<<<<<<< HEAD
215221
mock.brokerManager.listPersonas.mockReset()
216222
vi.mocked(loadStore).mockReset()
217223
vi.mocked(loadStore).mockReturnValue({ projects: [], activeProjectId: null })
@@ -347,3 +353,34 @@ describe('registerIpcHandlers git:generate-commit-message', () => {
347353
expect(mock.brokerManager.generateCommitDraft).not.toHaveBeenCalled()
348354
})
349355
})
356+
357+
describe('registerIpcHandlers broker:check-cli-available', () => {
358+
beforeEach(() => {
359+
mock.handlers.clear()
360+
mock.ipcMain.handle.mockClear()
361+
mock.ipcMain.on.mockClear()
362+
mock.isCommandAvailableWithAugmentedPath.mockReset()
363+
registerIpcHandlers()
364+
})
365+
366+
it('returns whether the requested CLI can be resolved', () => {
367+
const handler = mock.handlers.get('broker:check-cli-available')
368+
expect(handler).toBeTypeOf('function')
369+
mock.isCommandAvailableWithAugmentedPath.mockReturnValueOnce(true)
370+
371+
const result = handler?.({}, 'opencode')
372+
373+
expect(result).toBe(true)
374+
expect(mock.isCommandAvailableWithAugmentedPath).toHaveBeenCalledWith('opencode')
375+
})
376+
377+
it('rejects non-string CLI values', () => {
378+
const handler = mock.handlers.get('broker:check-cli-available')
379+
expect(handler).toBeTypeOf('function')
380+
381+
const result = handler?.({}, null)
382+
383+
expect(result).toBe(false)
384+
expect(mock.isCommandAvailableWithAugmentedPath).not.toHaveBeenCalled()
385+
})
386+
})

src/main/ipc-handlers.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
addProjectIntegration,
1818
removeProjectIntegration
1919
} from './store'
20-
import { brokerManager, resolveCommandWithAugmentedPath } from './broker'
20+
import { brokerManager, isCommandAvailableWithAugmentedPath } from './broker'
2121
import * as git from './git'
2222
import * as filesystem from './filesystem'
2323
import * as auth from './auth'
@@ -387,7 +387,7 @@ export function registerIpcHandlers(): void {
387387
})
388388

389389
ipcMain.handle('broker:check-cli-available', (_, cli: string) => {
390-
return Boolean(resolveCommandWithAugmentedPath(cli))
390+
return typeof cli === 'string' && isCommandAvailableWithAugmentedPath(cli)
391391
})
392392

393393
ipcMain.handle('broker:list-details', async () => {

src/renderer/src/components/sidebar/SpawnAgentDialog.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type React from 'react'
22
import { useCallback, useEffect, useRef, useState } from 'react'
33
import { Loader2, X } from 'lucide-react'
44
import { ClaudeIcon, CodexIcon, GrokIcon, OpenCodeIcon } from '@/components/common/AgentIcons'
5-
import { listProjectPersonas, spawnProjectAgent, spawnProjectPersona, type SpawnAgentCli } from '@/lib/spawn-agent'
5+
import { SPAWN_AGENT_CLI_INSTALL_COMMANDS, listProjectPersonas, spawnProjectAgent, spawnProjectPersona, type SpawnAgentCli } from '@/lib/spawn-agent'
66
import { pear, type WorkforcePersona } from '@/lib/ipc'
77
import { useProjectStore, type ProjectRoot } from '@/stores/project-store'
88
import { useUIStore } from '@/stores/ui-store'
@@ -231,7 +231,7 @@ export function SpawnAgentDialog(): React.ReactNode {
231231
className="flex min-h-[92px] flex-col items-center justify-center gap-2 rounded-lg border border-[var(--pear-border)] text-sm text-[var(--pear-text-dim)] hover:border-[var(--pear-accent-dim)] hover:text-[var(--pear-text)] disabled:cursor-not-allowed disabled:opacity-40"
232232
title={
233233
!root?.pathExists ? `Path not found: ${root?.path || project.rootPath}`
234-
: cliAvailability[cli] === false ? `${label} is not installed run: npm install -g ${cli}`
234+
: cliAvailability[cli] === false ? `${label} is not installed - run: ${SPAWN_AGENT_CLI_INSTALL_COMMANDS[cli]}`
235235
: `Spawn ${label}`
236236
}
237237
>

src/renderer/src/components/terminal/TerminalPane.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'
33
import { AlertTriangle, ChevronLeft, ChevronRight, Columns2, CornerUpLeft, Loader2, Network, PanelTop, X } from 'lucide-react'
44
import { AgentHarnessIcon, ClaudeIcon, CodexIcon, GrokIcon, OpenCodeIcon } from '@/components/common/AgentIcons'
55
import { ChatComposerInput } from '@/components/chat/ChatComposerInput'
6-
import { spawnProjectAgent, type SpawnAgentCli } from '@/lib/spawn-agent'
6+
import { SPAWN_AGENT_CLI_INSTALL_COMMANDS, spawnProjectAgent, type SpawnAgentCli } from '@/lib/spawn-agent'
77
import { formatTokenCount } from '@/lib/format'
88
import { pear, type BurnAgentInput, type BurnAgentSummary, type TerminalAttachMode } from '@/lib/ipc'
99
import { getAgentKeyForAgent, type Agent, useAgentStore } from '@/stores/agent-store'
@@ -803,7 +803,7 @@ export function TerminalPane(): React.ReactNode {
803803
className="flex items-center justify-center gap-2 rounded-lg border border-[var(--pear-border)] px-4 py-3 text-sm text-[var(--pear-text-dim)] hover:border-[var(--pear-accent-dim)] hover:text-[var(--pear-text)] disabled:cursor-not-allowed disabled:opacity-40"
804804
title={
805805
!activeRoot?.pathExists ? `Path not found: ${activeRoot?.path || activeProject.rootPath}`
806-
: cliAvailability[cli] === false ? `${label} is not installed`
806+
: cliAvailability[cli] === false ? `${label} is not installed - run: ${SPAWN_AGENT_CLI_INSTALL_COMMANDS[cli]}`
807807
: `Spawn ${label}`
808808
}
809809
>

src/renderer/src/lib/spawn-agent.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ import { useUIStore } from '@/stores/ui-store'
55

66
export type SpawnAgentCli = 'claude' | 'codex' | 'grok' | 'opencode'
77

8+
export const SPAWN_AGENT_CLI_INSTALL_COMMANDS: Record<SpawnAgentCli, string> = {
9+
claude: 'npm install -g @anthropic-ai/claude-code',
10+
codex: 'npm install -g @openai/codex',
11+
grok: 'npm install -g grok-ai',
12+
opencode: 'npm install -g opencode-ai'
13+
}
14+
815
function nextAgentName(cli: SpawnAgentCli, projectId: string, liveNames: string[]): string {
916
const existingNames = new Set([
1017
...liveNames,

0 commit comments

Comments
 (0)