diff --git a/desktop/src/agents/cli/cli-discovery.cjs b/desktop/src/agents/cli/cli-discovery.cjs index 66bc80c..ed51bac 100644 --- a/desktop/src/agents/cli/cli-discovery.cjs +++ b/desktop/src/agents/cli/cli-discovery.cjs @@ -219,7 +219,7 @@ function agentCommands(kind, options = {}) { const suffix = envValue(env, 'HERMES_DATA_DIR_SUFFIX', platform) || '' const homes = [configuredHome, pathApi.join(home, `.hermes${suffix}`)] .filter(value => typeof value === 'string' && pathApi.isAbsolute(value)) - const roots = [installDir, ...homes.map(value => pathApi.join(value, 'hermes-agent'))] + const roots = [installDir, ...homes.map(value => pathApi.join(value, 'hermes-agent')), ...homes] .filter(value => typeof value === 'string' && pathApi.isAbsolute(value)) for (const root of roots) { commands.push(pathApi.join(root, '.hermes', 'bin', 'hermes')) @@ -235,6 +235,7 @@ function agentCommands(kind, options = {}) { for (const root of homes) { commands.push(pathApi.join(root, 'bin', 'hermes')) commands.push(pathApi.join(root, 'hermes-agent', 'venv', 'Scripts', 'hermes')) + commands.push(pathApi.join(root, 'venv', 'Scripts', 'hermes')) } } if (kind === 'codex' && platform === 'darwin') { diff --git a/desktop/test/agents/cli/cli-adapters-discovery.test.cjs b/desktop/test/agents/cli/cli-adapters-discovery.test.cjs index cf51a0b..37c7092 100644 --- a/desktop/test/agents/cli/cli-adapters-discovery.test.cjs +++ b/desktop/test/agents/cli/cli-adapters-discovery.test.cjs @@ -538,6 +538,29 @@ test('Hermes discovery honors an explicit installation directory after PATH', as assert.equal(await resolveExecutable('hermes', options), '/test/bin/hermes') }) +test('Hermes discovers a flat installation in the selected native home', async () => { + const selected = '/test/flat-hermes' + const expected = `${selected}/.hermes/bin/hermes` + const options = { platform: 'darwin', home: '/test/home', + env: { PATH: '/missing', HERMES_HOME: selected }, + accessFn: async candidate => { if (candidate !== expected) throw new Error('missing') }, + execFileFn: async () => { throw new Error('missing') } } + assert.equal(await resolveExecutable('hermes', options), expected) + const agents = await detectAgents({ ...options, + nativeEnvironmentForKind: kind => kind === 'hermes' ? { HERMES_HOME: selected } : {}, + execFileFn: async (_command, args) => args[0] === '--version' + ? { stdout: 'Hermes 0.19.1', stderr: '' } : { stdout: '', stderr: '' }, + probeAgentCapabilitiesFn: async () => ({ compatibilityState: 'compatible' }), + }) + assert.equal(agents.find(agent => agent.kind === 'hermes')?.executable, expected) + const legacy = `${selected}/venv/bin/hermes` + options.accessFn = async candidate => { if (candidate !== legacy) throw new Error('missing') } + options.statFn = async () => { throw Object.assign(new Error('missing'), { code: 'ENOENT' }) } + assert.equal(await resolveExecutable('hermes', options), legacy) + options.statFn = async () => ({ isDirectory: () => true }) + assert.equal(await resolveExecutable('hermes', options), null) +}) + test('Hermes discovers a legacy Unix virtual environment only for legacy installs', async () => { const legacy = '/test/custom-install/venv/bin/hermes' const options = { platform: 'darwin', home: '/test/home', @@ -588,6 +611,9 @@ test('Windows Hermes discovery falls back to its native launcher and legacy virt assert.equal(await resolveExecutable('hermes', options), launcher) options.accessFn = async candidate => { if (candidate !== legacy) throw new Error('missing') } assert.equal(await resolveExecutable('hermes', options), legacy) + const flat = 'C:\\Native Hermes\\venv\\Scripts\\hermes.EXE' + options.accessFn = async candidate => { if (candidate !== flat) throw new Error('missing') } + assert.equal(await resolveExecutable('hermes', options), flat) options.accessFn = async candidate => { if (candidate !== 'C:\\missing\\hermes.EXE') throw new Error('missing') } assert.equal(await resolveExecutable('hermes', options), 'C:\\missing\\hermes.EXE') options.env = { PATH: 'C:\\missing', LOCALAPPDATA: 'C:\\Users\\Ryder\\AppData\\Local', PATHEXT: '.EXE' }