9a9416c99c
* feat: support profile-aware group chat bridge flows * feat: route cron jobs through hermes cli * Fix group chat routing and isolate bridge tests * Add Grok image-to-video media skill * Default Grok videos to media directory * Fix bridge profile fallback and cron repeat clearing * Refine bridge chat and gateway platform handling * Filter bridge tool-call text deltas * Preserve structured bridge chat history * Prepare beta release build artifacts * Fix Windows run profile resolution * Fix Windows path compatibility checks * Fix profile-scoped model page display * Hide Windows subprocess windows for jobs and updates * Hide Windows file backend subprocess windows * Avoid Windows gateway restart lock conflicts * Treat Windows gateway lock as running on startup * Force release Windows gateway lock on restart * Tighten Windows gateway lock cleanup * Update chat e2e source expectation * Bump package version to 0.5.30 --------- Co-authored-by: Codex <codex@openai.com>
157 lines
4.0 KiB
TypeScript
157 lines
4.0 KiB
TypeScript
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)
|
|
}
|
|
|
|
function getNodePrefix() {
|
|
return process.platform === 'win32' ? getNodeBinDir() : dirname(getNodeBinDir())
|
|
}
|
|
|
|
function getHomebrewPrefix() {
|
|
const match = process.execPath.match(/^(.*)\/Cellar\/[^/]+\/[^/]+\/bin\/node$/)
|
|
return match?.[1] || null
|
|
}
|
|
|
|
function getNpmCliCandidates() {
|
|
const prefix = getNodePrefix()
|
|
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'),
|
|
...(homebrewPrefix ? [join(homebrewPrefix, 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js')] : []),
|
|
]
|
|
}
|
|
|
|
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() {
|
|
return {
|
|
...process.env,
|
|
PATH: [getNodeBinDir(), process.env.PATH].filter(Boolean).join(delimiter),
|
|
npm_node_execpath: process.execPath,
|
|
}
|
|
}
|
|
|
|
function runNpm(args: string[], options: { timeout?: number } = {}) {
|
|
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'],
|
|
env: getCurrentNodeEnv(),
|
|
windowsHide: true,
|
|
}).trim()
|
|
}
|
|
|
|
function getGlobalRoot() {
|
|
return runNpm(['root', '-g'])
|
|
}
|
|
|
|
function getGlobalCliScript() {
|
|
const cli = getGlobalPackageBin(getGlobalRoot())
|
|
if (!existsSync(cli)) {
|
|
throw new Error(`Updated hermes-web-ui CLI not found: ${cli}`)
|
|
}
|
|
return cli
|
|
}
|
|
|
|
function runUpdateInstall() {
|
|
try {
|
|
runNpm(['cache', 'clean', '--force'], { timeout: 2 * 60 * 1000 })
|
|
} catch (err) {
|
|
console.warn('[update] failed to clean npm cache, continuing update:', err)
|
|
}
|
|
|
|
return runNpm(['install', '-g', 'hermes-web-ui@latest'], { timeout: 10 * 60 * 1000 })
|
|
}
|
|
|
|
function spawnRestart(port: string) {
|
|
const cli = getGlobalCliScript()
|
|
|
|
return spawn(process.execPath, [cli, 'restart', '--port', port], {
|
|
detached: true,
|
|
stdio: 'ignore',
|
|
windowsHide: true,
|
|
env: getCurrentNodeEnv(),
|
|
})
|
|
}
|
|
|
|
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()
|
|
|
|
ctx.body = {
|
|
success: true,
|
|
message: output.trim() || 'hermes-web-ui updated successfully',
|
|
}
|
|
|
|
setTimeout(() => {
|
|
let restart
|
|
try {
|
|
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
|
|
const failed = (typeof code === 'number' && code !== 0) || Boolean(signal)
|
|
if (failed) {
|
|
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,
|
|
message: err.stderr?.toString() || err.message || String(err),
|
|
}
|
|
}
|
|
}
|