[codex] fix self update restart (#707)
* fix: make self update restart reliably * chore: clarify update success message
This commit is contained in:
+42
-5
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -92,7 +92,7 @@ export default {
|
||||
updateVersion: 'v{version} にアップグレード',
|
||||
reloadClientVersion: 'v{version} に再読み込み',
|
||||
updating: '更新中...',
|
||||
updateSuccess: '更新が完了しました。サーバーを再起動してください',
|
||||
updateSuccess: '更新が完了しました。しばらくしてからページを再読み込みしてください。長時間起動しない場合は手動で起動してください。',
|
||||
updateFailed: '更新に失敗しました',
|
||||
logout: 'ログアウト',
|
||||
nodeVersionWarning: 'Node.js v{version} が検出されました。バージョン23以降にアップグレードしてください。',
|
||||
|
||||
@@ -92,7 +92,7 @@ export default {
|
||||
updateVersion: 'v{version}(으)로 업그레이드',
|
||||
reloadClientVersion: 'v{version}(으)로 새로고침',
|
||||
updating: '업데이트 중...',
|
||||
updateSuccess: '업데이트 완료, 서버를 재시작해 주세요',
|
||||
updateSuccess: '업데이트가 완료되었습니다. 잠시 후 페이지를 새로고침하세요. 오랫동안 시작되지 않으면 수동으로 시작하세요.',
|
||||
updateFailed: '업데이트 실패',
|
||||
logout: '로그아웃',
|
||||
nodeVersionWarning: 'Node.js v{version}이 감지되었습니다. 버전 23 이상으로 업그레이드하세요.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -104,7 +104,7 @@ export default {
|
||||
updateVersion: '升級版本 v{version}',
|
||||
reloadClientVersion: '重新整理到 v{version}',
|
||||
updating: '正在更新...',
|
||||
updateSuccess: '更新完成,請重新啟動服務',
|
||||
updateSuccess: '更新成功,請稍後重新整理頁面,如長時間未啟動,請手動啟動',
|
||||
updateFailed: '更新失敗',
|
||||
logout: '登出',
|
||||
nodeVersionWarning: '偵測到 Node.js v{version},請升級至 23 以上版本。',
|
||||
|
||||
@@ -104,7 +104,7 @@ export default {
|
||||
updateVersion: '升级版本 v{version}',
|
||||
reloadClientVersion: '刷新到 v{version}',
|
||||
updating: '正在更新...',
|
||||
updateSuccess: '更新完成,请重启服务',
|
||||
updateSuccess: '更新成功,请稍后刷新页面,如长时间未启动,请手动启动',
|
||||
updateFailed: '更新失败',
|
||||
logout: '退出登录',
|
||||
nodeVersionWarning: '检测到 Node.js v{version},请升级到23以上版本。',
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user