fix linux desktop packaging paths (#1162)
Co-authored-by: xingzhi <chuzihao.czh@alibaba-inc.com>
@@ -31,6 +31,7 @@ should not duplicate server persistence rules.
|
|||||||
- `HERMES_WEB_UI_HOME` and `HERMES_WEBUI_STATE_DIR` override Web UI state location.
|
- `HERMES_WEB_UI_HOME` and `HERMES_WEBUI_STATE_DIR` override Web UI state location.
|
||||||
- Hermes Agent state lives under Hermes profile directories and must stay distinct from Web UI state.
|
- Hermes Agent state lives under Hermes profile directories and must stay distinct from Web UI state.
|
||||||
- Uploads default to `config.uploadDir`, which is derived from the Web UI home unless `UPLOAD_DIR` is set.
|
- Uploads default to `config.uploadDir`, which is derived from the Web UI home unless `UPLOAD_DIR` is set.
|
||||||
|
- Runtime data directories must also live under the Web UI home, not beside built `dist` assets.
|
||||||
- Profile-scoped Hermes data should use existing profile helpers instead of manually joining paths.
|
- Profile-scoped Hermes data should use existing profile helpers instead of manually joining paths.
|
||||||
|
|
||||||
## Server Structure
|
## Server Structure
|
||||||
|
|||||||
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 821 B |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 179 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
@@ -21,6 +21,10 @@ files:
|
|||||||
# Web UI source (built dist) and bundled Python live outside the asar.
|
# Web UI source (built dist) and bundled Python live outside the asar.
|
||||||
# This package lives at packages/desktop, so ../.. is the hermes-web-ui repo root.
|
# This package lives at packages/desktop, so ../.. is the hermes-web-ui repo root.
|
||||||
extraResources:
|
extraResources:
|
||||||
|
- from: "build"
|
||||||
|
to: "build"
|
||||||
|
filter:
|
||||||
|
- "icon.png"
|
||||||
- from: "../.."
|
- from: "../.."
|
||||||
to: "webui"
|
to: "webui"
|
||||||
filter:
|
filter:
|
||||||
@@ -57,6 +61,7 @@ win:
|
|||||||
artifactName: "${productName}-${version}-${arch}.${ext}"
|
artifactName: "${productName}-${version}-${arch}.${ext}"
|
||||||
|
|
||||||
linux:
|
linux:
|
||||||
|
icon: build/icons
|
||||||
target:
|
target:
|
||||||
- target: AppImage
|
- target: AppImage
|
||||||
arch: [x64, arm64]
|
arch: [x64, arm64]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { app, BrowserWindow, Menu, shell, ipcMain } from 'electron'
|
import { app, BrowserWindow, Menu, shell, ipcMain } from 'electron'
|
||||||
import { join } from 'node:path'
|
import { join } from 'node:path'
|
||||||
import { startWebUiServer, stopWebUiServer, getToken } from './webui-server'
|
import { startWebUiServer, stopWebUiServer, getToken } from './webui-server'
|
||||||
import { hermesBinExists, hermesBin } from './paths'
|
import { desktopIcon, hermesBinExists, hermesBin } from './paths'
|
||||||
import { initAutoUpdater } from './updater'
|
import { initAutoUpdater } from './updater'
|
||||||
|
|
||||||
const PORT = Number(process.env.HERMES_DESKTOP_PORT) || 8748
|
const PORT = Number(process.env.HERMES_DESKTOP_PORT) || 8748
|
||||||
@@ -18,6 +18,7 @@ function createWindow() {
|
|||||||
title: 'Hermes Studio',
|
title: 'Hermes Studio',
|
||||||
backgroundColor: '#1a1a1a',
|
backgroundColor: '#1a1a1a',
|
||||||
autoHideMenuBar: true,
|
autoHideMenuBar: true,
|
||||||
|
...(process.platform === 'linux' ? { icon: desktopIcon() } : {}),
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: join(__dirname, '..', 'preload', 'index.js'),
|
preload: join(__dirname, '..', 'preload', 'index.js'),
|
||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ export function hermesBinExists(): boolean {
|
|||||||
return existsSync(hermesBin())
|
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 webUiHome(): string {
|
export function webUiHome(): string {
|
||||||
return process.env.HERMES_WEB_UI_HOME?.trim() || resolve(homedir(), '.hermes-web-ui')
|
return process.env.HERMES_WEB_UI_HOME?.trim() || resolve(homedir(), '.hermes-web-ui')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ export async function startWebUiServer(port = DEFAULT_PORT): Promise<string> {
|
|||||||
// macOS/Linux keep the standard ~/.hermes layout.
|
// macOS/Linux keep the standard ~/.hermes layout.
|
||||||
HERMES_HOME: agentHome,
|
HERMES_HOME: agentHome,
|
||||||
HERMES_WEB_UI_HOME: home,
|
HERMES_WEB_UI_HOME: home,
|
||||||
|
HERMES_WEBUI_STATE_DIR: home,
|
||||||
AUTH_TOKEN: token,
|
AUTH_TOKEN: token,
|
||||||
PORT: String(port),
|
PORT: String(port),
|
||||||
// Prepend bundled Python's bin to PATH so any incidental `python` resolution lands on ours
|
// Prepend bundled Python's bin to PATH so any incidental `python` resolution lands on ours
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { homedir } from 'os'
|
|||||||
* - HERMES_WEBUI_STATE_DIR: Compatibility alias for HERMES_WEB_UI_HOME.
|
* - HERMES_WEBUI_STATE_DIR: Compatibility alias for HERMES_WEB_UI_HOME.
|
||||||
* Default: join(homedir(), '.hermes-web-ui').
|
* Default: join(homedir(), '.hermes-web-ui').
|
||||||
* - UPLOAD_DIR: Upload directory override. Default: join(HERMES_WEB_UI_HOME, 'upload').
|
* - UPLOAD_DIR: Upload directory override. Default: join(HERMES_WEB_UI_HOME, 'upload').
|
||||||
|
* - dataDir: Internal Web UI runtime data directory. Default: join(HERMES_WEB_UI_HOME, 'data').
|
||||||
*
|
*
|
||||||
* Auth:
|
* Auth:
|
||||||
* - AUTH_TOKEN: Explicit bearer token. If unset, Web UI stores an auto-generated token under HERMES_WEB_UI_HOME.
|
* - AUTH_TOKEN: Explicit bearer token. If unset, Web UI stores an auto-generated token under HERMES_WEB_UI_HOME.
|
||||||
@@ -41,6 +42,10 @@ export function getWebUiHome(env: Record<string, string | undefined> = process.e
|
|||||||
return appHome ? resolve(appHome) : join(homedir(), '.hermes-web-ui')
|
return appHome ? resolve(appHome) : join(homedir(), '.hermes-web-ui')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getWebUiDataDir(env: Record<string, string | undefined> = process.env): string {
|
||||||
|
return join(getWebUiHome(env), 'data')
|
||||||
|
}
|
||||||
|
|
||||||
const appHome = getWebUiHome()
|
const appHome = getWebUiHome()
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
@@ -49,6 +54,6 @@ export const config = {
|
|||||||
host: getListenHost(),
|
host: getListenHost(),
|
||||||
appHome,
|
appHome,
|
||||||
uploadDir: process.env.UPLOAD_DIR || join(appHome, 'upload'),
|
uploadDir: process.env.UPLOAD_DIR || join(appHome, 'upload'),
|
||||||
dataDir: resolve(__dirname, '..', 'data'),
|
dataDir: getWebUiDataDir(),
|
||||||
corsOrigins: process.env.CORS_ORIGINS || '*',
|
corsOrigins: process.env.CORS_ORIGINS || '*',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ for (const dir of [
|
|||||||
'packages/client/src',
|
'packages/client/src',
|
||||||
'packages/server/src',
|
'packages/server/src',
|
||||||
'packages/desktop',
|
'packages/desktop',
|
||||||
|
'packages/desktop/build/icons',
|
||||||
'tests/client',
|
'tests/client',
|
||||||
'tests/server',
|
'tests/server',
|
||||||
'tests/e2e',
|
'tests/e2e',
|
||||||
@@ -50,6 +51,21 @@ for (const dir of [
|
|||||||
requireDir(dir)
|
requireDir(dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const icon of [
|
||||||
|
'packages/desktop/build/icon.png',
|
||||||
|
'packages/desktop/build/icon.icns',
|
||||||
|
'packages/desktop/build/icon.ico',
|
||||||
|
'packages/desktop/build/icons/16x16.png',
|
||||||
|
'packages/desktop/build/icons/32x32.png',
|
||||||
|
'packages/desktop/build/icons/48x48.png',
|
||||||
|
'packages/desktop/build/icons/64x64.png',
|
||||||
|
'packages/desktop/build/icons/128x128.png',
|
||||||
|
'packages/desktop/build/icons/256x256.png',
|
||||||
|
'packages/desktop/build/icons/512x512.png',
|
||||||
|
]) {
|
||||||
|
requireFile(icon)
|
||||||
|
}
|
||||||
|
|
||||||
const agents = await readText('AGENTS.md')
|
const agents = await readText('AGENTS.md')
|
||||||
const agentLines = agents.trimEnd().split(/\r?\n/)
|
const agentLines = agents.trimEnd().split(/\r?\n/)
|
||||||
if (agentLines.length > 120) {
|
if (agentLines.length > 120) {
|
||||||
@@ -101,10 +117,15 @@ if (!buildWorkflow.includes('npm run harness:check')) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const desktopReleaseWorkflow = await readText('.github/workflows/desktop-release.yml')
|
const desktopReleaseWorkflow = await readText('.github/workflows/desktop-release.yml')
|
||||||
|
const electronBuilderConfig = await readText('packages/desktop/electron-builder.yml')
|
||||||
if (!desktopReleaseWorkflow.includes('files: ${{ matrix.artifact_files }}')) {
|
if (!desktopReleaseWorkflow.includes('files: ${{ matrix.artifact_files }}')) {
|
||||||
fail('desktop-release.yml must upload matrix-specific artifact_files')
|
fail('desktop-release.yml must upload matrix-specific artifact_files')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!electronBuilderConfig.includes('icon: build/icons')) {
|
||||||
|
fail('electron-builder.yml must configure the Linux icon set')
|
||||||
|
}
|
||||||
|
|
||||||
for (const target of ['target_os: darwin', 'target_os: win32', 'target_os: linux']) {
|
for (const target of ['target_os: darwin', 'target_os: win32', 'target_os: linux']) {
|
||||||
if (!desktopReleaseWorkflow.includes(target)) {
|
if (!desktopReleaseWorkflow.includes(target)) {
|
||||||
fail(`desktop-release.yml is missing matrix target ${target}`)
|
fail(`desktop-release.yml is missing matrix target ${target}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
import { homedir } from 'os'
|
import { homedir } from 'os'
|
||||||
import { join, resolve } from 'path'
|
import { join, resolve } from 'path'
|
||||||
import { getListenHost, getWebUiHome } from '../../packages/server/src/config'
|
import { getListenHost, getWebUiDataDir, getWebUiHome } from '../../packages/server/src/config'
|
||||||
|
|
||||||
describe('server config', () => {
|
describe('server config', () => {
|
||||||
it('defaults to an IPv4 bind host', () => {
|
it('defaults to an IPv4 bind host', () => {
|
||||||
@@ -27,4 +27,8 @@ describe('server config', () => {
|
|||||||
it('uses HERMES_WEBUI_STATE_DIR as a compatibility alias', () => {
|
it('uses HERMES_WEBUI_STATE_DIR as a compatibility alias', () => {
|
||||||
expect(getWebUiHome({ HERMES_WEBUI_STATE_DIR: ' ./tmp/hermes-state ' })).toBe(resolve('./tmp/hermes-state'))
|
expect(getWebUiHome({ HERMES_WEBUI_STATE_DIR: ' ./tmp/hermes-state ' })).toBe(resolve('./tmp/hermes-state'))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('keeps runtime data under the web-ui home', () => {
|
||||||
|
expect(getWebUiDataDir({ HERMES_WEB_UI_HOME: ' ./tmp/hermes-ui ' })).toBe(resolve('./tmp/hermes-ui/data'))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||