Files
hermes-agent/apps/desktop/electron/source-python.ts
hermes-seaeye[bot] aae6c2044e fmt(js): npm run fix on merge (#123390)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-26 03:39:27 +00:00

58 lines
2.0 KiB
TypeScript

import { statSync } from 'node:fs'
import path from 'node:path'
export interface SourcePythonOptions {
/** Explicit developer interpreter (`HERMES_DESKTOP_PYTHON`), honoured as-is. */
override?: string
isWindows?: boolean
fileExists?: (candidate: string) => boolean
}
/** A checkout's own interpreter paths, in preference order. */
function interpreterPaths(isWindows: boolean): string[] {
return isWindows
? [path.join('.venv', 'Scripts', 'python.exe'), path.join('venv', 'Scripts', 'python.exe')]
: [path.join('.venv', 'bin', 'python'), path.join('venv', 'bin', 'python')]
}
function defaultFileExists(candidate: string): boolean {
try {
return statSync(candidate).isFile()
} catch {
return false
}
}
/**
* The interpreter that belongs to a source checkout: an explicit override, else
* the checkout's own virtualenv, else null.
*
* Never a PATH Python. A system interpreter can import a checkout while lacking
* its selected dependencies (`updater/checkout-source.ts`), so falling back
* silently turns "this root has no runtime" into an update probe or a backend
* running under an interpreter nothing selected — and a failed read becomes a
* wrong answer instead of a refusal. PM deletes the in-tree `venv`/`.venv` once
* a generation is committed, which makes null the ordinary answer for a managed
* install: its callers resolve the installation launcher instead
* (`resolveSourceInstallationBackend`, `readSourceUpdate`) or refuse
* (`preflightStateDb`), and the backend ladder moves on to its next rung.
*/
export function resolveSourcePython(
root: string,
{ override, isWindows = process.platform === 'win32', fileExists = defaultFileExists }: SourcePythonOptions = {}
): string | null {
if (override && fileExists(override)) {
return override
}
for (const relative of interpreterPaths(isWindows)) {
const candidate: string = path.join(root, relative)
if (fileExists(candidate)) {
return candidate
}
}
return null
}