2026-05-11 20:08:13 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* Hermes 路径检测工具 - 跨平台兼容
|
|
|
|
|
|
*
|
|
|
|
|
|
* Hermes 数据目录在不同平台上的位置:
|
2026-06-01 21:35:26 +08:00
|
|
|
|
* - Windows 原生安装: %LOCALAPPDATA%\hermes when it exists
|
2026-05-11 20:08:13 +08:00
|
|
|
|
* - Linux/macOS/WSL2: ~/.hermes
|
|
|
|
|
|
* - 用户自定义: HERMES_HOME 环境变量
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
2026-06-01 21:35:26 +08:00
|
|
|
|
import { existsSync } from 'fs'
|
2026-05-19 16:09:59 +08:00
|
|
|
|
import { basename, dirname, isAbsolute, relative, resolve, join } from 'path'
|
2026-05-11 20:08:13 +08:00
|
|
|
|
import { homedir } from 'os'
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 智能检测 Hermes 数据目录
|
|
|
|
|
|
*
|
|
|
|
|
|
* 检测优先级:
|
|
|
|
|
|
* 1. HERMES_HOME 环境变量(用户自定义)
|
2026-06-01 21:35:26 +08:00
|
|
|
|
* 2. Windows: existing %LOCALAPPDATA%\hermes or %APPDATA%\hermes
|
2026-05-11 20:08:13 +08:00
|
|
|
|
* 3. 默认: ~/.hermes(Linux/macOS/WSL2)
|
|
|
|
|
|
*
|
|
|
|
|
|
* @returns Hermes 数据目录的绝对路径
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function detectHermesHome(): string {
|
|
|
|
|
|
// 1. 用户自定义的环境变量(最高优先级)
|
|
|
|
|
|
if (process.env.HERMES_HOME) {
|
|
|
|
|
|
return resolve(process.env.HERMES_HOME)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-01 21:35:26 +08:00
|
|
|
|
const defaultHome = resolve(homedir(), '.hermes')
|
|
|
|
|
|
|
|
|
|
|
|
// 2. Windows:优先使用存在的原生安装数据目录;不存在时回退到 ~/.hermes。
|
2026-05-11 20:08:13 +08:00
|
|
|
|
if (process.platform === 'win32') {
|
2026-06-01 21:35:26 +08:00
|
|
|
|
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
|
2026-05-11 20:08:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. Linux/macOS:~/.hermes
|
2026-06-01 21:35:26 +08:00
|
|
|
|
return defaultHome
|
2026-05-11 20:08:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-16 20:27:23 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* Detect the Hermes root data directory.
|
|
|
|
|
|
*
|
|
|
|
|
|
* `HERMES_HOME` may intentionally point at a profile directory when launching a
|
|
|
|
|
|
* specific gateway (`<root>/profiles/<name>`). Web UI profile management needs
|
|
|
|
|
|
* the root directory so it can read `active_profile` and enumerate profiles.
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function detectHermesRootHome(): string {
|
|
|
|
|
|
const home = detectHermesHome()
|
|
|
|
|
|
const parent = dirname(home)
|
|
|
|
|
|
if (basename(parent) === 'profiles') return dirname(parent)
|
|
|
|
|
|
return home
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-11 20:08:13 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 获取 Hermes CLI 二进制文件路径
|
|
|
|
|
|
* @param customBin 自定义的 hermes 二进制路径
|
|
|
|
|
|
* @returns hermes 命令名称或路径
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function getHermesBin(customBin?: string): string {
|
|
|
|
|
|
if (customBin?.trim()) return customBin.trim()
|
|
|
|
|
|
if (process.env.HERMES_BIN?.trim()) return process.env.HERMES_BIN.trim()
|
|
|
|
|
|
return 'hermes'
|
|
|
|
|
|
}
|
2026-05-19 16:09:59 +08:00
|
|
|
|
|
|
|
|
|
|
function comparablePath(path: string): string {
|
|
|
|
|
|
return process.platform === 'win32' ? path.toLowerCase() : path
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function isPathWithin(targetPath: string, basePath: string): boolean {
|
|
|
|
|
|
const base = resolve(basePath)
|
|
|
|
|
|
const target = resolve(targetPath)
|
|
|
|
|
|
const rel = relative(comparablePath(base), comparablePath(target))
|
|
|
|
|
|
return rel === '' || (!!rel && !rel.startsWith('..') && !isAbsolute(rel))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function relativePathFromBase(targetPath: string, basePath: string): string | null {
|
|
|
|
|
|
if (!isPathWithin(targetPath, basePath)) return null
|
|
|
|
|
|
const rel = relative(resolve(basePath), resolve(targetPath))
|
|
|
|
|
|
return rel.replace(/\\/g, '/')
|
|
|
|
|
|
}
|