Add desktop (Electron) packaging and release distribution (#1147)
* Add desktop packaging workflow * Add desktop package homepage * Fix desktop default credential prompt * Suppress default credential prompt on desktop * Publish desktop artifacts on release; reduce CI to PR smoke test Add desktop-release.yml triggered on release publish (mirroring docker-publish.yml) to build all platforms and upload .dmg/.exe/ .AppImage/.deb to the GitHub Release. Trim build.yml desktop job to a PR-only Linux x64 smoke test, since release artifacts are now produced by desktop-release.yml. This drops per-push and macOS/Windows packaging from regular CI. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Fix desktop Hermes data home on Windows --------- Co-authored-by: xingzhi <chuzihao.czh@alibaba-inc.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env node
|
||||
// Install hermes-agent into the bundled Python at resources/python/<os>-<arch>/.
|
||||
// Prefers `uv` (10-100x faster, more deterministic) and falls back to pip.
|
||||
import { existsSync } from 'node:fs'
|
||||
import { resolve, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { platform as osPlatform, arch as osArch } from 'node:os'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const ROOT = resolve(__dirname, '..')
|
||||
|
||||
const TARGET_OS = process.env.TARGET_OS || osPlatform()
|
||||
const TARGET_ARCH = process.env.TARGET_ARCH || osArch()
|
||||
const HERMES_VERSION = process.env.HERMES_VERSION || '0.15.2'
|
||||
|
||||
const OS_LABEL = TARGET_OS === 'win32' ? 'win' : TARGET_OS === 'darwin' ? 'mac' : TARGET_OS
|
||||
const PY_DIR = resolve(ROOT, 'resources', 'python', `${OS_LABEL}-${TARGET_ARCH}`)
|
||||
|
||||
const pyBin = TARGET_OS === 'win32'
|
||||
? resolve(PY_DIR, 'python.exe')
|
||||
: resolve(PY_DIR, 'bin', 'python3')
|
||||
|
||||
if (!existsSync(pyBin)) {
|
||||
console.error(`Python not found at ${pyBin}. Run: npm run fetch:python`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
function hasUv() {
|
||||
const r = spawnSync('uv', ['--version'], { stdio: 'ignore' })
|
||||
return r.status === 0
|
||||
}
|
||||
|
||||
let r
|
||||
if (hasUv()) {
|
||||
console.log(`→ Installing hermes-agent==${HERMES_VERSION} via uv`)
|
||||
r = spawnSync('uv', [
|
||||
'pip', 'install',
|
||||
'--python', pyBin,
|
||||
`hermes-agent==${HERMES_VERSION}`,
|
||||
], { stdio: 'inherit' })
|
||||
} else {
|
||||
console.log(`→ Installing hermes-agent==${HERMES_VERSION} via pip`)
|
||||
r = spawnSync(pyBin, [
|
||||
'-m', 'pip', 'install',
|
||||
`hermes-agent==${HERMES_VERSION}`,
|
||||
'--no-warn-script-location',
|
||||
'--disable-pip-version-check',
|
||||
], { stdio: 'inherit' })
|
||||
}
|
||||
if (r.status !== 0) process.exit(r.status ?? 1)
|
||||
|
||||
const hermesBin = TARGET_OS === 'win32'
|
||||
? resolve(PY_DIR, 'Scripts', 'hermes.exe')
|
||||
: resolve(PY_DIR, 'bin', 'hermes')
|
||||
|
||||
if (!existsSync(hermesBin)) {
|
||||
console.error(`hermes binary not found at ${hermesBin} after install`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// hermes-web-ui's agent-bridge searches for `run_agent.py` at <python_root>/run_agent.py
|
||||
// (and a few neighbouring dirs). pip places it at site-packages/run_agent.py — surface
|
||||
// it at the venv root with a *relative* symlink so the venv stays portable when copied
|
||||
// into the packaged .app/.exe (an absolute symlink would break the moment the bundle
|
||||
// is moved to /Applications/...).
|
||||
const { readdirSync, symlinkSync, copyFileSync, unlinkSync, lstatSync } = await import('node:fs')
|
||||
function siteRunAgentRelative() {
|
||||
if (TARGET_OS === 'win32') {
|
||||
return ['Lib', 'site-packages', 'run_agent.py'].join('\\')
|
||||
}
|
||||
const libDir = resolve(PY_DIR, 'lib')
|
||||
const py = readdirSync(libDir).find(n => /^python\d+\.\d+$/.test(n))
|
||||
return ['lib', py, 'site-packages', 'run_agent.py'].join('/')
|
||||
}
|
||||
{
|
||||
const relSrc = siteRunAgentRelative()
|
||||
const absSrc = resolve(PY_DIR, relSrc)
|
||||
const dst = resolve(PY_DIR, 'run_agent.py')
|
||||
if (existsSync(absSrc)) {
|
||||
try { lstatSync(dst); unlinkSync(dst) } catch {}
|
||||
if (TARGET_OS === 'win32') copyFileSync(absSrc, dst)
|
||||
else symlinkSync(relSrc, dst)
|
||||
console.log(`✓ run_agent.py linked at venv root (relative → ${relSrc})`)
|
||||
} else {
|
||||
console.warn(`! run_agent.py not found at ${absSrc} — agent-bridge may fail`)
|
||||
}
|
||||
}
|
||||
|
||||
// Relocate: replace the pip-generated launcher (which embeds an absolute
|
||||
// shebang to the build-time Python path) with a relative wrapper so the
|
||||
// bundled venv works after being moved into the .app/.exe payload.
|
||||
const { writeFileSync, chmodSync } = await import('node:fs')
|
||||
if (TARGET_OS === 'win32') {
|
||||
// Windows: pip generates a .exe launcher that embeds a relative shebang
|
||||
// already. Add a .cmd wrapper that prefers the colocated python.exe.
|
||||
const cmdPath = resolve(PY_DIR, 'Scripts', 'hermes.cmd')
|
||||
writeFileSync(
|
||||
cmdPath,
|
||||
[
|
||||
'@echo off',
|
||||
'set "PY=%~dp0..\\python.exe"',
|
||||
'"%PY%" -m hermes_cli.main %*',
|
||||
].join('\r\n'),
|
||||
)
|
||||
} else {
|
||||
const launcher = [
|
||||
'#!/bin/sh',
|
||||
'DIR="$(cd "$(dirname "$0")" && pwd)"',
|
||||
'exec "$DIR/python3" -m hermes_cli.main "$@"',
|
||||
'',
|
||||
].join('\n')
|
||||
writeFileSync(hermesBin, launcher, { mode: 0o755 })
|
||||
chmodSync(hermesBin, 0o755)
|
||||
// Same for hermes-agent / hermes-acp (they all just dispatch into modules)
|
||||
for (const [name, mod] of [
|
||||
['hermes-agent', 'run_agent'],
|
||||
['hermes-acp', 'acp_adapter.entry'],
|
||||
]) {
|
||||
const p = resolve(PY_DIR, 'bin', name)
|
||||
if (existsSync(p)) {
|
||||
writeFileSync(p, launcher.replace('hermes_cli.main', mod), { mode: 0o755 })
|
||||
chmodSync(p, 0o755)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✓ hermes installed at ${hermesBin} (relocatable launcher)`)
|
||||
|
||||
r = spawnSync(hermesBin, ['--version'], { stdio: 'inherit' })
|
||||
if (r.status !== 0) {
|
||||
console.error('hermes --version failed')
|
||||
process.exit(r.status ?? 1)
|
||||
}
|
||||
Reference in New Issue
Block a user