import { app } from 'electron' import { existsSync, readdirSync } from 'node:fs' import { join, resolve } from 'node:path' import { homedir, platform, arch } from 'node:os' const isWin = platform() === 'win32' const osLabel = isWin ? 'win' : platform() === 'darwin' ? 'mac' : platform() // mac | linux | win const archLabel = arch() // arm64 | x64 export function isPackaged() { return app.isPackaged } // Bundled web-ui directory. // dev: (or HERMES_WEB_UI_DIR) // prod: /webui export function webuiDir(): string { if (app.isPackaged) return resolve(process.resourcesPath, 'webui') return process.env.HERMES_WEB_UI_DIR?.trim() || resolve(app.getAppPath(), '..', '..') } export function webuiServerEntry(): string { return join(webuiDir(), 'dist', 'server', 'index.js') } export function runtimePlatformKey(): string { return `${osLabel}-${archLabel}` } export function desktopRuntimeDir(): string { const override = process.env.HERMES_DESKTOP_RUNTIME_DIR?.trim() if (override) return resolve(override) return join(webUiHome(), 'desktop-runtime', runtimePlatformKey()) } function packagedResourceDir(name: string): string { return resolve(process.resourcesPath, name) } // dev: packages/desktop/resources/python/- // prod: /python when present, otherwise downloaded runtime cache. export function pythonDir(): string { if (app.isPackaged) { const packaged = packagedResourceDir('python') return existsSync(packaged) ? packaged : join(desktopRuntimeDir(), 'python') } return resolve(app.getAppPath(), 'resources', 'python', runtimePlatformKey()) } export function nodeDir(): string { if (app.isPackaged) { const packaged = packagedResourceDir('node') return existsSync(packaged) ? packaged : join(desktopRuntimeDir(), 'node') } return resolve(app.getAppPath(), 'resources', 'node', runtimePlatformKey()) } export function nodeBinDir(): string { const dir = nodeDir() return isWin ? dir : join(dir, 'bin') } export function bundledNode(): string { return isWin ? join(nodeDir(), 'node.exe') : join(nodeBinDir(), 'node') } export function gitDir(): string { if (app.isPackaged) { const packaged = packagedResourceDir('git') return existsSync(packaged) ? packaged : join(desktopRuntimeDir(), 'git') } return resolve(app.getAppPath(), 'resources', 'git', runtimePlatformKey()) } export function gitPathDirs(): string[] { if (!isWin) return [] const dir = gitDir() return [ join(dir, 'cmd'), join(dir, 'mingw64', 'bin'), join(dir, 'usr', 'bin'), ].filter(existsSync) } export function bundledGit(): string | undefined { if (!isWin) return undefined const git = join(gitDir(), 'cmd', 'git.exe') return existsSync(git) ? git : undefined } export function bundledAgentBrowserHome(): string { return join(pythonDir(), 'agent-browser') } function browserExecutableNames(): Set { if (isWin) return new Set(['chrome.exe']) if (platform() === 'darwin') return new Set(['Google Chrome for Testing', 'Google Chrome', 'Chromium', 'chrome']) return new Set(['chrome', 'chromium', 'chromium-browser']) } export function bundledBrowserExecutable(): string | undefined { const names = browserExecutableNames() const stack = [join(bundledAgentBrowserHome(), 'browsers'), bundledAgentBrowserHome()].filter(existsSync) const visited = new Set() while (stack.length > 0) { const dir = stack.pop() if (!dir || visited.has(dir)) continue visited.add(dir) let entries try { entries = readdirSync(dir, { withFileTypes: true }) } catch { continue } for (const entry of entries) { const path = join(dir, entry.name) if (entry.isFile() && names.has(entry.name)) return path if (entry.isDirectory()) stack.push(path) } } return undefined } export function pythonBinDir(): string { const dir = pythonDir() return isWin ? join(dir, 'Scripts') : join(dir, 'bin') } export function bundledPython(): string { const dir = pythonDir() return isWin ? join(dir, 'python.exe') : join(dir, 'bin', 'python3') } export function hermesBin(): string { return isWin ? join(pythonBinDir(), 'hermes.exe') : join(pythonBinDir(), 'hermes') } export function hermesBinExists(): boolean { return existsSync(hermesBin()) } export function desktopIcon(): string { if (app.isPackaged) return resolve(process.resourcesPath, 'build', 'icon.png') return resolve(app.getAppPath(), 'build', 'icon.png') } export function desktopWindowsTrayIcon(): string { if (app.isPackaged) return resolve(process.resourcesPath, 'build', 'trayWindows.png') return resolve(app.getAppPath(), 'build', 'trayWindows.png') } export function desktopTrayTemplateIcon(): string { if (app.isPackaged) return resolve(process.resourcesPath, 'build', 'trayTemplate.png') return resolve(app.getAppPath(), 'build', 'trayTemplate.png') } export function webUiHome(): string { return process.env.HERMES_WEB_UI_HOME?.trim() || resolve(homedir(), '.hermes-web-ui') } export function hermesHome(): string { const override = process.env.HERMES_HOME?.trim() if (override) return resolve(override) const defaultHome = resolve(homedir(), '.hermes') if (isWin) { const candidates = [ process.env.LOCALAPPDATA, process.env.APPDATA, ] .map(value => value?.trim()) .filter((value): value is string => !!value) .map(value => resolve(value, 'hermes')) for (const candidate of candidates) { if (existsSync(candidate)) return candidate } } return defaultHome } export function tokenFile(): string { return join(webUiHome(), '.token') }