chore: bootstrap hermes-ui skeleton from 0.6.29 snapshot
Build / build (push) Canceled after 0s
NPM Lockfile Check / npm ci --ignore-scripts (push) Canceled after 0s
Playwright / e2e (push) Canceled after 0s

Import the client, server, skills, tests, and build scripts as the starting
point for the rebuild, then trim the surfaces this project does not ship:
Electron desktop packaging, the marketing website, and the ESP32 firmware
tree. CI keeps the web build, e2e, lockfile, release, and Docker workflows.

Rename the package and CLI entry points to hermes-ui, drop the prepare hook so
npm ci no longer triggers a build, and drop the removed tsconfig and nodemon
references. Rewrite AGENTS.md, ARCHITECTURE.md, DEVELOPMENT.md, and the READMEs
so the documented package map and hard rules match this repository.

reference_code/ is ignored: it is a read-only snapshot, not an input to the build.
This commit is contained in:
2026-10-16 10:34:35 +08:00
commit 1e9ea89d86
912 changed files with 287328 additions and 0 deletions
+135
View File
@@ -0,0 +1,135 @@
import { logger } from './logger'
import { closeDb } from '../db'
import { stopPreviewRuntime } from '../controllers/update'
import { codingAgentRunManager } from './agent-runner/coding-agent-run-manager'
import { shutdownManagedGateways } from './hermes/gateway-runner'
import { stopOutboundRelayClient } from './global-agent/outbound-relay-client'
const DEFAULT_SHUTDOWN_FORCE_EXIT_MS = 15_000
const DEFAULT_DESKTOP_SHUTDOWN_FORCE_EXIT_MS = 15_000
function envPositiveInt(name: string): number | undefined {
const value = Number(process.env[name])
return Number.isFinite(value) && value > 0 ? value : undefined
}
export function getShutdownForceExitMs(): number {
const override = envPositiveInt('HERMES_WEB_UI_SHUTDOWN_FORCE_EXIT_MS')
if (override) return override
const desktop = String(process.env.HERMES_DESKTOP || '').trim().toLowerCase() === 'true'
return desktop ? DEFAULT_DESKTOP_SHUTDOWN_FORCE_EXIT_MS : DEFAULT_SHUTDOWN_FORCE_EXIT_MS
}
export function shouldStopAgentBridgeOnShutdown(_signal: string): boolean {
const raw = String(process.env.HERMES_AGENT_BRIDGE_STOP_ON_SHUTDOWN || '').trim().toLowerCase()
if (['1', 'true', 'yes', 'on'].includes(raw)) return true
if (['0', 'false', 'no', 'off'].includes(raw)) return false
// Restart now defaults to a fresh bridge so package/runtime upgrades do not
// attach to stale brokers. Operators can opt back into restart resume with
// HERMES_AGENT_BRIDGE_STOP_ON_SHUTDOWN=0.
return true
}
export function shouldStopManagedGatewaysOnShutdown(env: NodeJS.ProcessEnv = process.env): boolean {
const raw = String(env.HERMES_WEB_UI_STOP_GATEWAYS_ON_SHUTDOWN || '').trim().toLowerCase()
if (['1', 'true', 'yes', 'on'].includes(raw)) return true
if (['0', 'false', 'no', 'off'].includes(raw)) return false
return String(env.NODE_ENV || '').trim().toLowerCase() === 'production'
}
export type ShutdownHandler = (signal: string) => Promise<void>
export function createShutdownHandler(server: any, groupChatServer?: any, chatRunServer?: any, agentBridgeManager?: any): ShutdownHandler {
let isShuttingDown = false
return async (signal: string) => {
if (isShuttingDown) return
isShuttingDown = true
// Force exit only if graceful cleanup hangs. The bridge can take up to 10s
// to stop worker subprocesses, so this cap must be longer than that.
setTimeout(() => process.exit(0), getShutdownForceExitMs())
logger.info('Shutting down (%s)...', signal)
console.log(`[shutdown] Received signal: ${signal}`)
try {
try {
await stopPreviewRuntime()
logger.info('Preview runtime stopped')
} catch (err) {
logger.warn(err, 'Failed to stop preview runtime (non-fatal)')
}
if (shouldStopManagedGatewaysOnShutdown()) {
try {
const result = await shutdownManagedGateways()
logger.info('[shutdown] managed gateways stopped result=%j', result)
} catch (err) {
logger.warn(err, 'Failed to stop managed gateways (non-fatal)')
}
} else {
logger.info('[shutdown] leaving managed gateways running')
}
if (agentBridgeManager && shouldStopAgentBridgeOnShutdown(signal)) {
try {
await agentBridgeManager.stop()
logger.info('Agent bridge stopped')
} catch (err) {
logger.warn(err, 'Failed to stop agent bridge (non-fatal)')
}
} else if (agentBridgeManager) {
logger.info('Leaving agent bridge running across Web UI shutdown')
}
// Close ChatRunSocket first to release WebSocket state.
if (chatRunServer) {
chatRunServer.close()
logger.info('ChatRunSocket closed')
}
stopOutboundRelayClient()
logger.info('Outbound relay clients closed')
codingAgentRunManager.shutdown()
logger.info('Coding agent hidden sessions closed')
// Disconnect Socket.IO before HTTP server to prevent hanging
if (groupChatServer) {
groupChatServer.agentClients.disconnectAll()
groupChatServer.getIO().close()
logger.info('Socket.IO closed')
}
const servers = Array.isArray(server) ? server : [server].filter(Boolean)
if (servers.length) {
await Promise.all(servers.map((httpServer) => (
new Promise<void>((resolve) => {
httpServer.close(() => {
logger.info('HTTP server closed')
resolve()
})
})
)))
}
} catch (err) {
logger.error(err, 'Shutdown error')
}
closeDb()
process.exit(0)
}
}
export function bindShutdown(server: any, groupChatServer?: any, chatRunServer?: any, agentBridgeManager?: any): ShutdownHandler {
const shutdown = createShutdownHandler(server, groupChatServer, chatRunServer, agentBridgeManager)
process.once('SIGUSR2', shutdown)
process.on('SIGINT', shutdown)
process.on('SIGTERM', shutdown)
return shutdown
}