diff --git a/bin/hermes-web-ui.mjs b/bin/hermes-web-ui.mjs index 85c8af8..f3f41e6 100755 --- a/bin/hermes-web-ui.mjs +++ b/bin/hermes-web-ui.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { spawn, execSync } from 'child_process' -import { resolve, dirname, join } from 'path' +import { spawn, execSync, execFileSync } from 'child_process' +import { resolve, dirname, join, delimiter } from 'path' import { fileURLToPath } from 'url' import { readFileSync, writeFileSync, unlinkSync, mkdirSync, openSync, chmodSync, statSync, existsSync } from 'fs' import { randomBytes } from 'crypto' @@ -56,8 +56,27 @@ function getNpmBin() { return join(getNodeBinDir(), process.platform === 'win32' ? 'npm.cmd' : 'npm') } -function getCliBin() { - return join(getNodeBinDir(), process.platform === 'win32' ? 'hermes-web-ui.cmd' : 'hermes-web-ui') +function getCurrentNodeEnv() { + return { + ...process.env, + PATH: [getNodeBinDir(), process.env.PATH].filter(Boolean).join(delimiter), + npm_node_execpath: process.execPath, + } +} + +function getGlobalPrefix() { + return execFileSync(getNpmBin(), ['prefix', '-g'], { + encoding: 'utf-8', + stdio: ['pipe', 'pipe', 'pipe'], + env: getCurrentNodeEnv(), + }).trim() +} + +function getGlobalCliBin() { + const prefix = getGlobalPrefix() + return process.platform === 'win32' + ? join(prefix, 'hermes-web-ui.cmd') + : join(prefix, 'bin', 'hermes-web-ui') } function getWindowsShell() { @@ -431,18 +450,36 @@ function doUpdate() { const child = spawnCli(getNpmBin(), ['install', '-g', 'hermes-web-ui@latest'], { stdio: 'inherit', windowsHide: true, + env: getCurrentNodeEnv(), + }) + + child.on('error', (err) => { + console.log(` ✗ Update failed: ${err.message}`) + process.exit(1) }) child.on('exit', (code) => { if (code === 0) { console.log(' ✓ Update complete, restarting...') - const restart = spawnCli(getCliBin(), ['restart', '--port', String(getUpdatePort())], { + const cli = getGlobalCliBin() + if (!existsSync(cli)) { + console.log(` ✗ Updated CLI not found: ${cli}`) + process.exit(1) + } + + const restart = spawnCli(cli, ['restart', '--port', String(getUpdatePort())], { stdio: 'inherit', windowsHide: true, + env: getCurrentNodeEnv(), + }) + restart.on('error', (err) => { + console.log(` ✗ Restart failed: ${err.message}`) + process.exit(1) }) restart.on('exit', (restartCode) => process.exit(restartCode ?? 1)) } else { console.log(' ✗ Update failed') + process.exit(code ?? 1) } }) } diff --git a/package.json b/package.json index a29a85b..ae3db0a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hermes-web-ui", - "version": "0.5.20", + "version": "0.5.21", "description": "Self-hosted AI chat dashboard for Hermes Agent — multi-model web UI with multi-platform integration", "repository": { "type": "git", diff --git a/packages/client/src/data/changelog.ts b/packages/client/src/data/changelog.ts index 3134124..4cca838 100644 --- a/packages/client/src/data/changelog.ts +++ b/packages/client/src/data/changelog.ts @@ -6,7 +6,7 @@ export interface ChangelogEntry { export const changelog: ChangelogEntry[] = [ { - version: '0.5.20', + version: '0.5.21', date: '2026-05-14', changes: [ 'changelog.new_0_5_18_1', diff --git a/packages/client/src/i18n/locales/de.ts b/packages/client/src/i18n/locales/de.ts index 98fa6b9..11e2469 100644 --- a/packages/client/src/i18n/locales/de.ts +++ b/packages/client/src/i18n/locales/de.ts @@ -92,7 +92,7 @@ export default { updateVersion: 'Aktualisieren auf v{version}', reloadClientVersion: 'Für v{version} neu laden', updating: 'Aktualisierung...', - updateSuccess: 'Aktualisierung abgeschlossen, bitte Server neu starten', + updateSuccess: 'Aktualisierung erfolgreich. Bitte aktualisieren Sie die Seite in Kurze. Wenn der Dienst langere Zeit nicht startet, starten Sie ihn manuell.', updateFailed: 'Aktualisierung fehlgeschlagen', logout: 'Abmelden', nodeVersionWarning: 'Node.js v{version} erkannt. Bitte aktualisieren Sie auf Version 23 oder neuer.', diff --git a/packages/client/src/i18n/locales/en.ts b/packages/client/src/i18n/locales/en.ts index 01869b9..eadd5c5 100644 --- a/packages/client/src/i18n/locales/en.ts +++ b/packages/client/src/i18n/locales/en.ts @@ -104,7 +104,7 @@ export default { updateVersion: 'Upgrade to v{version}', reloadClientVersion: 'Reload for v{version}', updating: 'Updating...', - updateSuccess: 'Update complete, please restart the server', + updateSuccess: 'Update successful. Please refresh the page shortly. If it does not start after a while, start it manually.', updateFailed: 'Update failed', logout: 'Sign Out', nodeVersionWarning: 'Detected Node.js v{version}. Please upgrade to version 23 or later.', diff --git a/packages/client/src/i18n/locales/es.ts b/packages/client/src/i18n/locales/es.ts index 0b23d3c..6f73d36 100644 --- a/packages/client/src/i18n/locales/es.ts +++ b/packages/client/src/i18n/locales/es.ts @@ -92,7 +92,7 @@ export default { updateVersion: 'Actualizar a v{version}', reloadClientVersion: 'Recargar para v{version}', updating: 'Actualizando...', - updateSuccess: 'Actualizacion completa, por favor reinicia el servidor', + updateSuccess: 'Actualizacion completada. Actualiza la pagina en breve. Si no se inicia despues de un tiempo, inicialo manualmente.', updateFailed: 'Error al actualizar', logout: 'Cerrar sesion', nodeVersionWarning: 'Se detecto Node.js v{version}. Actualiza a la version 23 o posterior.', diff --git a/packages/client/src/i18n/locales/fr.ts b/packages/client/src/i18n/locales/fr.ts index 7a36771..8872776 100644 --- a/packages/client/src/i18n/locales/fr.ts +++ b/packages/client/src/i18n/locales/fr.ts @@ -92,7 +92,7 @@ export default { updateVersion: 'Mettre a jour vers v{version}', reloadClientVersion: 'Recharger pour v{version}', updating: 'Mise a jour...', - updateSuccess: 'Mise a jour terminee, veuillez redemarrer le serveur', + updateSuccess: 'Mise a jour terminee. Veuillez actualiser la page sous peu. Si le service ne demarre pas apres un moment, demarrez-le manuellement.', updateFailed: 'Echec de la mise a jour', logout: 'Deconnexion', nodeVersionWarning: 'Node.js v{version} detecte. Veuillez passer a la version 23 ou ulterieure.', diff --git a/packages/client/src/i18n/locales/ja.ts b/packages/client/src/i18n/locales/ja.ts index 1bbaf25..08697af 100644 --- a/packages/client/src/i18n/locales/ja.ts +++ b/packages/client/src/i18n/locales/ja.ts @@ -92,7 +92,7 @@ export default { updateVersion: 'v{version} にアップグレード', reloadClientVersion: 'v{version} に再読み込み', updating: '更新中...', - updateSuccess: '更新が完了しました。サーバーを再起動してください', + updateSuccess: '更新が完了しました。しばらくしてからページを再読み込みしてください。長時間起動しない場合は手動で起動してください。', updateFailed: '更新に失敗しました', logout: 'ログアウト', nodeVersionWarning: 'Node.js v{version} が検出されました。バージョン23以降にアップグレードしてください。', diff --git a/packages/client/src/i18n/locales/ko.ts b/packages/client/src/i18n/locales/ko.ts index 7de6dc9..1eb0625 100644 --- a/packages/client/src/i18n/locales/ko.ts +++ b/packages/client/src/i18n/locales/ko.ts @@ -92,7 +92,7 @@ export default { updateVersion: 'v{version}(으)로 업그레이드', reloadClientVersion: 'v{version}(으)로 새로고침', updating: '업데이트 중...', - updateSuccess: '업데이트 완료, 서버를 재시작해 주세요', + updateSuccess: '업데이트가 완료되었습니다. 잠시 후 페이지를 새로고침하세요. 오랫동안 시작되지 않으면 수동으로 시작하세요.', updateFailed: '업데이트 실패', logout: '로그아웃', nodeVersionWarning: 'Node.js v{version}이 감지되었습니다. 버전 23 이상으로 업그레이드하세요.', diff --git a/packages/client/src/i18n/locales/pt.ts b/packages/client/src/i18n/locales/pt.ts index 7d0472c..870f7fc 100644 --- a/packages/client/src/i18n/locales/pt.ts +++ b/packages/client/src/i18n/locales/pt.ts @@ -92,7 +92,7 @@ export default { updateVersion: 'Atualizar para v{version}', reloadClientVersion: 'Recarregar para v{version}', updating: 'Atualizando...', - updateSuccess: 'Atualizacao concluida, por favor reinicie o servidor', + updateSuccess: 'Atualizacao concluida. Atualize a pagina em breve. Se nao iniciar apos algum tempo, inicie manualmente.', updateFailed: 'Falha na atualizacao', logout: 'Sair', nodeVersionWarning: 'Node.js v{version} detectado. Atualize para a versao 23 ou posterior.', diff --git a/packages/client/src/i18n/locales/zh-TW.ts b/packages/client/src/i18n/locales/zh-TW.ts index cb4d77b..77b2861 100644 --- a/packages/client/src/i18n/locales/zh-TW.ts +++ b/packages/client/src/i18n/locales/zh-TW.ts @@ -104,7 +104,7 @@ export default { updateVersion: '升級版本 v{version}', reloadClientVersion: '重新整理到 v{version}', updating: '正在更新...', - updateSuccess: '更新完成,請重新啟動服務', + updateSuccess: '更新成功,請稍後重新整理頁面,如長時間未啟動,請手動啟動', updateFailed: '更新失敗', logout: '登出', nodeVersionWarning: '偵測到 Node.js v{version},請升級至 23 以上版本。', diff --git a/packages/client/src/i18n/locales/zh.ts b/packages/client/src/i18n/locales/zh.ts index 2af7459..3aee9ba 100644 --- a/packages/client/src/i18n/locales/zh.ts +++ b/packages/client/src/i18n/locales/zh.ts @@ -104,7 +104,7 @@ export default { updateVersion: '升级版本 v{version}', reloadClientVersion: '刷新到 v{version}', updating: '正在更新...', - updateSuccess: '更新完成,请重启服务', + updateSuccess: '更新成功,请稍后刷新页面,如长时间未启动,请手动启动', updateFailed: '更新失败', logout: '退出登录', nodeVersionWarning: '检测到 Node.js v{version},请升级到23以上版本。', diff --git a/packages/server/src/controllers/update.ts b/packages/server/src/controllers/update.ts index c61a626..4e80d5d 100644 --- a/packages/server/src/controllers/update.ts +++ b/packages/server/src/controllers/update.ts @@ -2,6 +2,8 @@ import { execFileSync, spawn } from 'child_process' import { existsSync } from 'fs' import { delimiter, dirname, join } from 'path' +let updateInProgress = false + function getNodeBinDir() { return dirname(process.execPath) } @@ -10,27 +12,39 @@ function getNodePrefix() { return process.platform === 'win32' ? getNodeBinDir() : dirname(getNodeBinDir()) } -function getNpmCliPath() { +function getHomebrewPrefix() { + const match = process.execPath.match(/^(.*)\/Cellar\/[^/]+\/[^/]+\/bin\/node$/) + return match?.[1] || null +} + +function getNpmCliCandidates() { const prefix = getNodePrefix() - const candidates = process.platform === 'win32' + const homebrewPrefix = getHomebrewPrefix() + + return process.platform === 'win32' ? [ join(prefix, 'node_modules', 'npm', 'bin', 'npm-cli.js'), join(getNodeBinDir(), 'node_modules', 'npm', 'bin', 'npm-cli.js'), ] - : [join(prefix, 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js')] - const npmCli = candidates.find(existsSync) - - if (!npmCli) { - throw new Error(`Unable to locate npm CLI for ${process.execPath}; checked ${candidates.join(', ')}`) - } - - return npmCli + : [ + join(prefix, 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'), + ...(homebrewPrefix ? [join(homebrewPrefix, 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js')] : []), + ] } -function getGlobalPackageBin(prefix: string) { - return process.platform === 'win32' - ? join(prefix, 'node_modules', 'hermes-web-ui', 'bin', 'hermes-web-ui.mjs') - : join(prefix, 'lib', 'node_modules', 'hermes-web-ui', 'bin', 'hermes-web-ui.mjs') +function getNpmCliPath() { + const candidates = getNpmCliCandidates() + const npmCli = candidates.find(existsSync) + + return npmCli || null +} + +function getNpmBin() { + return process.platform === 'win32' ? 'npm.cmd' : 'npm' +} + +function getGlobalPackageBin(root: string) { + return join(root, 'hermes-web-ui', 'bin', 'hermes-web-ui.mjs') } function getCurrentNodeEnv() { @@ -42,7 +56,11 @@ function getCurrentNodeEnv() { } function runNpm(args: string[], options: { timeout?: number } = {}) { - return execFileSync(process.execPath, [getNpmCliPath(), ...args], { + const npmCli = getNpmCliPath() + const command = npmCli ? process.execPath : getNpmBin() + const commandArgs = npmCli ? [npmCli, ...args] : args + + return execFileSync(command, commandArgs, { encoding: 'utf-8', timeout: options.timeout, stdio: ['pipe', 'pipe', 'pipe'], @@ -50,12 +68,16 @@ function runNpm(args: string[], options: { timeout?: number } = {}) { }).trim() } -function getGlobalPrefix() { - return runNpm(['prefix', '-g']) +function getGlobalRoot() { + return runNpm(['root', '-g']) } function getGlobalCliScript() { - return getGlobalPackageBin(getGlobalPrefix()) + const cli = getGlobalPackageBin(getGlobalRoot()) + if (!existsSync(cli)) { + throw new Error(`Updated hermes-web-ui CLI not found: ${cli}`) + } + return cli } function runUpdateInstall() { @@ -74,6 +96,17 @@ function spawnRestart(port: string) { } export async function handleUpdate(ctx: any) { + if (updateInProgress) { + ctx.status = 409 + ctx.body = { + success: false, + message: 'hermes-web-ui update is already in progress', + } + return + } + + updateInProgress = true + try { const output = runUpdateInstall() @@ -83,13 +116,27 @@ export async function handleUpdate(ctx: any) { } setTimeout(() => { + let restart try { - spawnRestart(process.env.PORT || '8648').unref() - } finally { - process.exit(0) + restart = spawnRestart(process.env.PORT || '8648') + } catch (err) { + updateInProgress = false + console.error('[update] failed to spawn restart:', err) + return } + + restart.on('error', (err) => { + updateInProgress = false + console.error('[update] restart process failed:', err) + }) + restart.on('exit', (code, signal) => { + updateInProgress = false + console.error(`[update] restart process exited before replacing server: code=${code} signal=${signal}`) + }) + restart.unref() }, 3000) } catch (err: any) { + updateInProgress = false ctx.status = 500 ctx.body = { success: false,