Phase 004: remove the desktop shell, website tooling, and bundled hermes CLI

- Drop all desktop/Electron branches, scripts, and UI (title bar, pets,
  desktop bridge); LAN endpoint kind is now web/custom only
- Replace the bundled hermes CLI with user-side installation: add
  GET /api/hermes/cli-status plus a HermesMissingView onboarding guide
- Remove the version-management UI (snapshots handled by git) and prune
  dead website/mcu/claude scripts and lockfile entries
This commit is contained in:
2026-10-16 15:33:56 +08:00
parent 8c1ddf415f
commit 4701bf0ad0
49 changed files with 354 additions and 2403 deletions
-70
View File
@@ -11,12 +11,10 @@ import {
gatewayStateLooksRunningForProfile,
parseGatewayStatusesFromProfileListOutput,
prepareGatewayForProfileDelete,
recoverWindowsDesktopGatewayOrphans,
reconcileGatewayManagementTransition,
resolveGatewayTargetProfile,
selectGatewayProfilesForAutostart,
selectProfilesForGatewayAutostart,
shouldRecoverWindowsDesktopGatewayOrphans,
shouldUseUnifiedGatewayManagement,
shouldUseManagedGatewayRun,
shouldUseManagedGatewayRunForAutostart,
@@ -251,74 +249,6 @@ describe('gateway autostart status parsing', () => {
}
})
it('only recovers Windows desktop gateway orphans when enabled', () => {
expect(shouldRecoverWindowsDesktopGatewayOrphans('win32', { HERMES_DESKTOP: 'true' })).toBe(true)
expect(shouldRecoverWindowsDesktopGatewayOrphans('darwin', { HERMES_DESKTOP: 'true' })).toBe(false)
expect(shouldRecoverWindowsDesktopGatewayOrphans('win32', {})).toBe(false)
expect(shouldRecoverWindowsDesktopGatewayOrphans('win32', {
HERMES_DESKTOP: 'true',
HERMES_WEB_UI_DISABLE_GATEWAY_STARTUP_RECOVERY: '1',
})).toBe(false)
})
it('kills Windows desktop gateway runtime PIDs and removes stale runtime files', async () => {
const home = mkdtempSync(join(tmpdir(), 'hermes-gateway-recovery-'))
const workHome = join(home, 'profiles', 'work')
mkdirSync(workHome, { recursive: true })
const killed: number[] = []
const stopped: string[] = []
try {
writeFileSync(join(home, 'gateway.pid'), JSON.stringify({ pid: 11111 }), 'utf-8')
writeFileSync(join(home, 'gateway_state.json'), JSON.stringify({ pid: 99999, gateway_state: 'stopped' }), 'utf-8')
writeFileSync(join(workHome, 'gateway.lock'), JSON.stringify({ pid: '22222' }), 'utf-8')
writeFileSync(join(workHome, 'gateway_state.json'), JSON.stringify({ pid: 33333, gateway_state: 'running' }), 'utf-8')
const result = await recoverWindowsDesktopGatewayOrphans({
platform: 'win32',
env: { HERMES_DESKTOP: 'true' },
hermesHome: home,
isAlive: pid => pid !== 99999,
stopGateway: async profileDir => { stopped.push(profileDir) },
execTaskkill: async pid => { killed.push(pid) },
})
expect(result.attempted).toBe(true)
expect(stopped.sort()).toEqual([home, workHome].sort())
expect(result.stoppedProfileDirs.sort()).toEqual([home, workHome].sort())
expect(killed.sort((a, b) => a - b)).toEqual([11111, 22222, 33333])
expect(result.killedPids.sort((a, b) => a - b)).toEqual([11111, 22222, 33333])
expect(existsSync(join(home, 'gateway.pid'))).toBe(false)
expect(existsSync(join(home, 'gateway_state.json'))).toBe(false)
expect(existsSync(join(workHome, 'gateway.lock'))).toBe(false)
expect(existsSync(join(workHome, 'gateway_state.json'))).toBe(false)
} finally {
rmSync(home, { recursive: true, force: true })
}
})
it('skips recovery outside Windows desktop mode without deleting runtime files', async () => {
const home = mkdtempSync(join(tmpdir(), 'hermes-gateway-recovery-skip-'))
try {
const pidPath = join(home, 'gateway.pid')
writeFileSync(pidPath, JSON.stringify({ pid: 11111 }), 'utf-8')
const result = await recoverWindowsDesktopGatewayOrphans({
platform: 'linux',
env: { HERMES_DESKTOP: 'true' },
hermesHome: home,
isAlive: () => {
throw new Error('should not check liveness')
},
})
expect(result.attempted).toBe(false)
expect(existsSync(pidPath)).toBe(true)
} finally {
rmSync(home, { recursive: true, force: true })
}
})
it('detects managed gateway state files with a live pid', () => {
const dir = mkdtempSync(join(tmpdir(), 'hermes-gateway-state-'))
try {
+1 -1
View File
@@ -60,7 +60,7 @@ describe('LAN discovery', () => {
it('classifies well-known LAN endpoints', () => {
expect(getLanEndpointKind(8648)).toBe('web')
expect(getLanEndpointKind(8748)).toBe('desktop')
expect(getLanEndpointKind(8748)).toBe('custom')
expect(getLanEndpointKind(19001)).toBe('custom')
})
+1 -9
View File
@@ -9,7 +9,6 @@ describe('shutdown bridge policy', () => {
const originalValue = process.env.HERMES_AGENT_BRIDGE_STOP_ON_SHUTDOWN
const originalGatewayValue = process.env.HERMES_WEB_UI_STOP_GATEWAYS_ON_SHUTDOWN
const originalNodeEnv = process.env.NODE_ENV
const originalDesktop = process.env.HERMES_DESKTOP
const originalForceExitMs = process.env.HERMES_WEB_UI_SHUTDOWN_FORCE_EXIT_MS
afterEach(() => {
@@ -19,8 +18,6 @@ describe('shutdown bridge policy', () => {
else process.env.HERMES_WEB_UI_STOP_GATEWAYS_ON_SHUTDOWN = originalGatewayValue
if (originalNodeEnv === undefined) delete process.env.NODE_ENV
else process.env.NODE_ENV = originalNodeEnv
if (originalDesktop === undefined) delete process.env.HERMES_DESKTOP
else process.env.HERMES_DESKTOP = originalDesktop
if (originalForceExitMs === undefined) delete process.env.HERMES_WEB_UI_SHUTDOWN_FORCE_EXIT_MS
else process.env.HERMES_WEB_UI_SHUTDOWN_FORCE_EXIT_MS = originalForceExitMs
})
@@ -62,18 +59,13 @@ describe('shutdown bridge policy', () => {
expect(shouldStopManagedGatewaysOnShutdown()).toBe(false)
})
it('keeps desktop shutdown force-exit timing long enough for runtime cleanup by default', () => {
it('keeps shutdown force-exit timing long enough for runtime cleanup by default', () => {
delete process.env.HERMES_WEB_UI_SHUTDOWN_FORCE_EXIT_MS
delete process.env.HERMES_DESKTOP
expect(getShutdownForceExitMs()).toBe(15_000)
process.env.HERMES_DESKTOP = 'true'
expect(getShutdownForceExitMs()).toBe(15_000)
})
it('allows operators to override shutdown force-exit timing', () => {
process.env.HERMES_DESKTOP = 'true'
process.env.HERMES_WEB_UI_SHUTDOWN_FORCE_EXIT_MS = '7000'
expect(getShutdownForceExitMs()).toBe(7_000)
+1 -3
View File
@@ -31,7 +31,6 @@ describe('studio MCP autoinject', () => {
beforeEach(() => {
vi.resetModules()
vi.clearAllMocks()
delete process.env.HERMES_DESKTOP
delete process.env.HERMES_AGENT_NODE
delete process.env.AUTH_TOKEN
delete process.env.HERMES_WEB_UI_DISABLE_MCP_AUTOINJECT
@@ -206,8 +205,7 @@ describe('studio MCP autoinject', () => {
expect(updated.data.mcp_servers['hermes-studio-use'].args).toEqual([join(process.cwd(), 'bin/hermes-ui-mcp.mjs'), 'use'])
})
it('uses the desktop runtime node for bundled MCP servers when available', async () => {
process.env.HERMES_DESKTOP = 'true'
it('uses the runtime node override for bundled MCP servers when available', async () => {
process.env.HERMES_AGENT_NODE = '/runtime/node'
const { injectBundledMcpServer } = await import('../../packages/server/src/services/hermes/studio-mcp-autoinject')