Phase 002: rebrand to hermes-ui and consolidate i18n to zh/en
Build / build (push) Canceled after 0s
NPM Lockfile Check / npm ci --ignore-scripts (push) Canceled after 0s
Playwright / e2e (push) Canceled after 0s

Removes the ekko / hermes-web-ui branding from the whole tree and drops
eight locale files, so the project presents one name and one language pair.

Data compatibility is deliberate, not accidental: DB filenames, the JWT
audience, and skill manifest ownership all still recognise their pre-rename
values so existing deployments under ~/.hermes-web-ui keep working without a
re-login or a re-injection.

scripts/harness-check.mjs now scans for the legacy strings and fails the
build on any new occurrence, with an explicit allow-list (and reasons) for
the compatibility paths that must keep them.
This commit is contained in:
2026-10-16 11:11:29 +08:00
parent 1e9ea89d86
commit c93e1fe1bd
70 changed files with 1305 additions and 17856 deletions
+3 -3
View File
@@ -80,7 +80,7 @@ function makeSession(title: string): Session {
describe('ChatView tab title', () => {
beforeEach(() => {
vi.clearAllMocks()
document.title = 'Hermes Studio'
document.title = 'hermes-ui'
setActivePinia(createPinia())
const appStore = useAppStore()
@@ -107,7 +107,7 @@ describe('ChatView tab title', () => {
expect(document.title).toBe('Implementation Notes')
wrapper.unmount()
expect(document.title).toBe('Hermes Studio')
expect(document.title).toBe('hermes-ui')
})
it('falls back to the product title when the session title is blank', () => {
@@ -116,7 +116,7 @@ describe('ChatView tab title', () => {
const wrapper = mount(ChatView)
expect(document.title).toBe('Hermes Studio')
expect(document.title).toBe('hermes-ui')
wrapper.unmount()
})
})
-16
View File
@@ -6,14 +6,6 @@ import { changelog } from '@/data/changelog'
import { messages, supportedLocales } from '@/i18n/messages'
import en from '@/i18n/locales/en'
import zh from '@/i18n/locales/zh'
import zhTW from '@/i18n/locales/zh-TW'
import ja from '@/i18n/locales/ja'
import ko from '@/i18n/locales/ko'
import fr from '@/i18n/locales/fr'
import es from '@/i18n/locales/es'
import de from '@/i18n/locales/de'
import pt from '@/i18n/locales/pt'
import ru from '@/i18n/locales/ru'
import { createI18n } from 'vue-i18n'
const SOURCE_ROOT = join(process.cwd(), 'packages/client/src')
@@ -23,14 +15,6 @@ const allMessages: Record<string, Record<string, unknown>> = { en }
const rawMessages: Record<string, Record<string, unknown>> = {
en,
zh,
'zh-TW': zhTW,
ja,
ko,
fr,
es,
de,
pt,
ru,
}
function walkFiles(dir: string, files: string[] = []): string[] {
+2 -2
View File
@@ -117,8 +117,8 @@ describe('LoginView password login', () => {
expect(wrapper.find('.login-lock-hint').text()).toContain('login.defaultLoginResetHint')
const commands = wrapper.findAll('.login-lock-hint code').map(command => command.text())
expect(commands).toEqual([
'hermes-web-ui clear-login-locks --restart',
'hermes-web-ui reset-default-login',
'hermes-ui clear-login-locks --restart',
'hermes-ui reset-default-login',
])
})
+2 -2
View File
@@ -7,13 +7,13 @@ describe('PWA metadata', () => {
expect(html).toContain('rel="manifest" href="/manifest.webmanifest"')
expect(html).toContain('rel="apple-touch-icon" href="/logo.png"')
expect(html).toContain('name="apple-mobile-web-app-title" content="Hermes Studio"')
expect(html).toContain('name="apple-mobile-web-app-title" content="hermes-ui"')
})
it('ships a standalone web manifest with the Hermes icon', () => {
const manifest = JSON.parse(readFileSync('packages/client/public/manifest.webmanifest', 'utf8'))
expect(manifest.name).toBe('Hermes Studio')
expect(manifest.name).toBe('hermes-ui')
expect(manifest.display).toBe('standalone')
expect(manifest.start_url).toBe('/#/hermes/chat')
expect(manifest.icons).toEqual(expect.arrayContaining([
+6 -6
View File
@@ -19,7 +19,7 @@ async function loadCli(overrides: Partial<ChildProcessMocks> = {}) {
vi.resetModules()
vi.doMock('child_process', () => ({ execFileSync, execSync, spawn }))
const mod = await import('../../bin/hermes-web-ui.mjs')
const mod = await import('../../bin/hermes-ui.mjs')
return {
...mod,
mocks: { execFileSync, execSync, spawn },
@@ -166,20 +166,20 @@ describe('CLI port detection', () => {
it('skips browser launch when --no-open is provided', async () => {
const { shouldOpenBrowser } = await loadCli()
expect(shouldOpenBrowser(['node', 'bin/hermes-web-ui.mjs', 'start'])).toBe(true)
expect(shouldOpenBrowser(['node', 'bin/hermes-web-ui.mjs', 'start', '--no-open'])).toBe(false)
expect(shouldOpenBrowser(['node', 'bin/hermes-web-ui.mjs', 'restart', '--port', '9000', '--no-open'])).toBe(false)
expect(shouldOpenBrowser(['node', 'bin/hermes-ui.mjs', 'start'])).toBe(true)
expect(shouldOpenBrowser(['node', 'bin/hermes-ui.mjs', 'start', '--no-open'])).toBe(false)
expect(shouldOpenBrowser(['node', 'bin/hermes-ui.mjs', 'restart', '--port', '9000', '--no-open'])).toBe(false)
})
it('preserves --no-open when update respawns restart', async () => {
const { getRestartArgs } = await loadCli()
expect(getRestartArgs(9000, ['node', 'bin/hermes-web-ui.mjs', 'update', '--port', '9000'])).toEqual([
expect(getRestartArgs(9000, ['node', 'bin/hermes-ui.mjs', 'update', '--port', '9000'])).toEqual([
'restart',
'--port',
'9000',
])
expect(getRestartArgs(9000, ['node', 'bin/hermes-web-ui.mjs', 'update', '--port', '9000', '--no-open'])).toEqual([
expect(getRestartArgs(9000, ['node', 'bin/hermes-ui.mjs', 'update', '--port', '9000', '--no-open'])).toEqual([
'restart',
'--port',
'9000',
+7 -7
View File
@@ -220,7 +220,7 @@ describe('coding agent launch preparation', () => {
const mcp = JSON.parse(readFileSync(join(result.rootDir, 'mcp.json'), 'utf-8'))
expect(mcp.mcpServers['hermes-studio-api']).toMatchObject({
command: process.execPath,
args: [join(process.cwd(), 'bin/hermes-studio-mcp.mjs'), 'api'],
args: [join(process.cwd(), 'bin/hermes-ui-mcp.mjs'), 'api'],
env: {
HERMES_WEB_UI_URL: 'http://127.0.0.1:8648',
HERMES_WEB_UI_HOME: home,
@@ -233,7 +233,7 @@ describe('coding agent launch preparation', () => {
})
expect(mcp.mcpServers['hermes-studio-devices']).toMatchObject({
command: process.execPath,
args: [join(process.cwd(), 'bin/hermes-studio-mcp.mjs'), 'devices'],
args: [join(process.cwd(), 'bin/hermes-ui-mcp.mjs'), 'devices'],
env: {
HERMES_MCP_SERVER_NAME: 'hermes-studio-devices',
HERMES_MCP_TOOLSET: 'devices',
@@ -241,7 +241,7 @@ describe('coding agent launch preparation', () => {
})
expect(mcp.mcpServers['hermes-studio-use']).toMatchObject({
command: process.execPath,
args: [join(process.cwd(), 'bin/hermes-studio-mcp.mjs'), 'use'],
args: [join(process.cwd(), 'bin/hermes-ui-mcp.mjs'), 'use'],
env: {
HERMES_MCP_SERVER_NAME: 'hermes-studio-use',
HERMES_MCP_TOOLSET: 'use',
@@ -268,7 +268,7 @@ describe('coding agent launch preparation', () => {
const mcp = JSON.parse(readFileSync(join(result.rootDir, 'mcp.json'), 'utf-8'))
expect(mcp.mcpServers['hermes-studio-api']).toMatchObject({
command: '/runtime/node',
args: [join(process.cwd(), 'bin/hermes-studio-mcp.mjs'), 'api'],
args: [join(process.cwd(), 'bin/hermes-ui-mcp.mjs'), 'api'],
env: {
HERMES_WEB_UI_HOME: home,
HERMES_MCP_SERVER_NAME: 'hermes-studio-api',
@@ -544,9 +544,9 @@ describe('coding agent launch preparation', () => {
expect(config).toContain('[mcp_servers.hermes-studio-devices]')
expect(config).toContain('[mcp_servers.hermes-studio-use]')
expect(config).toContain(`command = "${process.execPath}"`)
expect(config).toContain(`args = ["${join(process.cwd(), 'bin/hermes-studio-mcp.mjs')}", "api"]`)
expect(config).toContain(`args = ["${join(process.cwd(), 'bin/hermes-studio-mcp.mjs')}", "devices"]`)
expect(config).toContain(`args = ["${join(process.cwd(), 'bin/hermes-studio-mcp.mjs')}", "use"]`)
expect(config).toContain(`args = ["${join(process.cwd(), 'bin/hermes-ui-mcp.mjs')}", "api"]`)
expect(config).toContain(`args = ["${join(process.cwd(), 'bin/hermes-ui-mcp.mjs')}", "devices"]`)
expect(config).toContain(`args = ["${join(process.cwd(), 'bin/hermes-ui-mcp.mjs')}", "use"]`)
expect(config).toContain(`env = { HERMES_WEB_UI_URL = "http://127.0.0.1:8648", HERMES_WEB_UI_HOME = "${home}"`)
expect(config).toContain('HERMES_WEBUI_STATE_DIR = "')
expect(config).toContain('HERMES_WEB_UI_PROFILE = "default"')
+3 -3
View File
@@ -29,7 +29,7 @@ describe('Ekko MCP server context', () => {
expect(servers['hermes-studio-api']).toEqual({
command: process.execPath,
args: [join(process.cwd(), 'bin/hermes-studio-mcp.mjs'), 'api'],
args: [join(process.cwd(), 'bin/hermes-ui-mcp.mjs'), 'api'],
env: {
HERMES_WEB_UI_URL: 'http://127.0.0.1:8648',
HERMES_WEB_UI_HOME: '/Users/test/.hermes-web-ui',
@@ -42,7 +42,7 @@ describe('Ekko MCP server context', () => {
enabled: true,
})
expect(servers['hermes-studio-devices']).toMatchObject({
args: [join(process.cwd(), 'bin/hermes-studio-mcp.mjs'), 'devices'],
args: [join(process.cwd(), 'bin/hermes-ui-mcp.mjs'), 'devices'],
env: {
HERMES_WEB_UI_URL: 'http://127.0.0.1:8648',
HERMES_WEB_UI_PROFILE: 'work',
@@ -50,7 +50,7 @@ describe('Ekko MCP server context', () => {
},
})
expect(servers['hermes-studio-use']).toMatchObject({
args: [join(process.cwd(), 'bin/hermes-studio-mcp.mjs'), 'use'],
args: [join(process.cwd(), 'bin/hermes-ui-mcp.mjs'), 'use'],
env: {
HERMES_WEB_UI_URL: 'http://127.0.0.1:8648',
HERMES_WEB_UI_PROFILE: 'work',
@@ -27,7 +27,7 @@ function waitForRpc(responses: Map<number, any>, id: number): Promise<any> {
})
}
describe('hermes-web-ui MCP server', () => {
describe('hermes-ui MCP server', () => {
let child: ChildProcessWithoutNullStreams | null = null
const homes: string[] = []
@@ -38,7 +38,7 @@ describe('hermes-web-ui MCP server', () => {
})
it('exposes a public Web UI API requester tool', async () => {
const home = mkdtempSync(join(tmpdir(), 'hermes-web-ui-mcp-'))
const home = mkdtempSync(join(tmpdir(), 'hermes-ui-mcp-'))
homes.push(home)
mkdirSync(join(home, 'profiles', 'research'), { recursive: true })
writeFileSync(join(home, 'profiles', 'research', '.model-run-token'), 'profile-token\n')
@@ -130,7 +130,7 @@ describe('hermes-web-ui MCP server', () => {
if (!address || typeof address === 'string') throw new Error('expected TCP server address')
const responses = new Map<number, any>()
child = spawn(process.execPath, ['bin/hermes-studio-mcp.mjs'], {
child = spawn(process.execPath, ['bin/hermes-ui-mcp.mjs'], {
cwd: process.cwd(),
env: {
...process.env,
@@ -189,7 +189,7 @@ describe('hermes-web-ui MCP server', () => {
const initialized = await waitForRpc(responses, 1)
expect(initialized.result.serverInfo).toMatchObject({
name: 'hermes-studio-mcp',
name: 'hermes-ui-mcp',
version: pkg.version,
})
expect(initialized.result.capabilities).toEqual({ tools: {} })
@@ -293,8 +293,7 @@ describe('hermes-web-ui MCP server', () => {
})
it.each([
'bin/hermes-studio-mcp.mjs',
'bin/hermes-web-ui-mcp.mjs',
'bin/hermes-ui-mcp.mjs',
])('reports the package version from the CLI entry %s', async (entry) => {
const child = spawn(process.execPath, [entry, '--version'], {
cwd: process.cwd(),
@@ -305,7 +304,7 @@ describe('hermes-web-ui MCP server', () => {
const code = await new Promise<number | null>(resolve => child.on('close', resolve))
expect(code).toBe(0)
expect(stdout.trim()).toBe(`hermes-studio-mcp v${pkg.version}`)
expect(stdout.trim()).toBe(`hermes-ui-mcp v${pkg.version}`)
})
it('exposes curated Hermes Studio use tools in the use toolset', async () => {
@@ -492,7 +491,7 @@ describe('hermes-web-ui MCP server', () => {
if (!address || typeof address === 'string') throw new Error('expected TCP server address')
const responses = new Map<number, any>()
child = spawn(process.execPath, ['bin/hermes-studio-mcp.mjs', 'use'], {
child = spawn(process.execPath, ['bin/hermes-ui-mcp.mjs', 'use'], {
cwd: process.cwd(),
env: {
...process.env,
@@ -804,7 +803,7 @@ describe('hermes-web-ui MCP server', () => {
if (!address || typeof address === 'string') throw new Error('expected TCP server address')
const responses = new Map<number, any>()
child = spawn(process.execPath, ['bin/hermes-studio-mcp.mjs', 'devices'], {
child = spawn(process.execPath, ['bin/hermes-ui-mcp.mjs', 'devices'], {
cwd: process.cwd(),
env: {
...process.env,
+1 -1
View File
@@ -68,7 +68,7 @@ describe('Hermes jobs controller', () => {
beforeEach(() => {
vi.clearAllMocks()
tempDir = mkdtempSync(join(tmpdir(), 'hermes-web-ui-jobs-test-'))
tempDir = mkdtempSync(join(tmpdir(), 'hermes-ui-jobs-test-'))
testState.profileDir = tempDir
testState.execFile.mockImplementation((_bin, _args, _opts, cb) => {
cb(null, { stdout: '', stderr: '' })
+1 -1
View File
@@ -194,7 +194,7 @@ describe('LAN discovery', () => {
})
it('exposes an MCP terminal list tool so agents can recover forgotten terminal ids', () => {
const mcpSource = readFileSync('bin/hermes-studio-mcp.mjs', 'utf8')
const mcpSource = readFileSync('bin/hermes-ui-mcp.mjs', 'utf8')
expect(mcpSource).toContain("name: 'hermes_studio_lan_devices_list'")
expect(mcpSource).toContain('online status')
+1 -1
View File
@@ -9,7 +9,7 @@ async function loadLimiter() {
}))
vi.doMock('fs', () => ({ writeFileSync: vi.fn() }))
vi.doMock('../../packages/server/src/config', () => ({
config: { appHome: '/tmp/hermes-web-ui-test' },
config: { appHome: '/tmp/hermes-ui-test' },
}))
return import('../../packages/server/src/services/login-limiter')
}
+7 -7
View File
@@ -18,20 +18,20 @@ afterEach(() => {
describe('media controller', () => {
it('uses Hermes Web UI media directory as the default generated video output path', async () => {
process.env.HERMES_WEB_UI_HOME = '/tmp/hermes-web-ui-test-home'
process.env.HERMES_WEB_UI_HOME = '/tmp/hermes-ui-test-home'
const { defaultImageOutputPath, defaultMediaOutputPath } = await import('../../packages/server/src/controllers/hermes/media')
expect(defaultMediaOutputPath('req_123')).toBe(join('/tmp/hermes-web-ui-test-home', 'media', 'req_123.mp4'))
expect(defaultMediaOutputPath('bad/request:id')).toBe(join('/tmp/hermes-web-ui-test-home', 'media', 'bad_request_id.mp4'))
expect(defaultImageOutputPath('img_123')).toBe(join('/tmp/hermes-web-ui-test-home', 'media', 'img_123.png'))
expect(defaultImageOutputPath('bad/request:id', 1)).toBe(join('/tmp/hermes-web-ui-test-home', 'media', 'bad_request_id-2.png'))
expect(defaultMediaOutputPath('req_123')).toBe(join('/tmp/hermes-ui-test-home', 'media', 'req_123.mp4'))
expect(defaultMediaOutputPath('bad/request:id')).toBe(join('/tmp/hermes-ui-test-home', 'media', 'bad_request_id.mp4'))
expect(defaultImageOutputPath('img_123')).toBe(join('/tmp/hermes-ui-test-home', 'media', 'img_123.png'))
expect(defaultImageOutputPath('bad/request:id', 1)).toBe(join('/tmp/hermes-ui-test-home', 'media', 'bad_request_id-2.png'))
})
it('generates images through the requested configured custom provider', async () => {
vi.stubEnv('AGNES_API_KEY', 'agnes-secret')
vi.doMock('../../packages/server/src/services/hermes/hermes-profile', () => ({
getActiveProfileName: () => 'default',
getProfileDir: () => '/tmp/hermes-web-ui-test-profile',
getProfileDir: () => '/tmp/hermes-ui-test-profile',
listProfileNamesFromDisk: () => ['default'],
}))
vi.doMock('../../packages/server/src/services/config-helpers', () => ({
@@ -60,7 +60,7 @@ describe('media controller', () => {
provider: 'agnes',
mode: 'text',
prompt: 'make an icon',
output_path: '/tmp/hermes-web-ui-agnes-image.png',
output_path: '/tmp/hermes-ui-agnes-image.png',
},
},
get: vi.fn(() => ''),
+3 -3
View File
@@ -315,7 +315,7 @@ describe('Profile Routes', () => {
describe('profile avatars', () => {
it('stores generated avatar metadata under the Web UI home', async () => {
const webUiHome = await mkdtemp(join(tmpdir(), 'hermes-web-ui-avatar-'))
const webUiHome = await mkdtemp(join(tmpdir(), 'hermes-ui-avatar-'))
tempHomes.push(webUiHome)
process.env.HERMES_WEB_UI_HOME = webUiHome
const { updateAvatar } = await import('../../packages/server/src/controllers/hermes/profiles')
@@ -338,7 +338,7 @@ describe('Profile Routes', () => {
})
it('stores uploaded image avatars and returns a data URL', async () => {
const webUiHome = await mkdtemp(join(tmpdir(), 'hermes-web-ui-avatar-'))
const webUiHome = await mkdtemp(join(tmpdir(), 'hermes-ui-avatar-'))
tempHomes.push(webUiHome)
process.env.HERMES_WEB_UI_HOME = webUiHome
const dataUrl = `data:image/png;base64,${Buffer.from('avatar-png').toString('base64')}`
@@ -361,7 +361,7 @@ describe('Profile Routes', () => {
})
it('deletes profile avatar metadata', async () => {
const webUiHome = await mkdtemp(join(tmpdir(), 'hermes-web-ui-avatar-'))
const webUiHome = await mkdtemp(join(tmpdir(), 'hermes-ui-avatar-'))
tempHomes.push(webUiHome)
process.env.HERMES_WEB_UI_HOME = webUiHome
const metadataDir = join(webUiHome, 'profile-metadata', Buffer.from('work', 'utf-8').toString('base64url'))
+3 -3
View File
@@ -65,7 +65,7 @@ describe('HermesSkillInjector', () => {
expect(result.skipped).toEqual(['existing-skill'])
await expect(readFile(join(hermesHome, 'skills', 'new-skill', 'SKILL.md'), 'utf-8')).resolves.toBe('# New Skill\n')
await expect(readFile(join(hermesHome, 'skills', 'existing-skill', 'SKILL.md'), 'utf-8')).resolves.toBe('# User Existing\n')
await expect(readManifest(join(hermesHome, 'skills'))).resolves.toMatchObject({ 'new-skill': { owner: 'hermes-web-ui' } })
await expect(readManifest(join(hermesHome, 'skills'))).resolves.toMatchObject({ 'new-skill': { owner: 'hermes-ui' } })
expect(existsSync(join(hermesHome, 'skills', 'new-skill', '.wui-managed.json'))).toBe(false)
expect(existsSync(join(hermesHome, 'skills', 'existing-skill', '.wui-managed.json'))).toBe(false)
})
@@ -89,7 +89,7 @@ describe('HermesSkillInjector', () => {
expect(result.updated).toEqual(['webui-skill'])
expect(result.skipped).toEqual([])
await expect(readFile(join(hermesHome, 'skills', 'webui-skill', 'SKILL.md'), 'utf-8')).resolves.toBe('# WebUI Skill v2\n')
await expect(readManifest(join(hermesHome, 'skills'))).resolves.toMatchObject({ 'webui-skill': { owner: 'hermes-web-ui' } })
await expect(readManifest(join(hermesHome, 'skills'))).resolves.toMatchObject({ 'webui-skill': { owner: 'hermes-ui' } })
})
it('ignores common OS metadata files when deciding whether a managed copy can update', async () => {
@@ -130,7 +130,7 @@ describe('HermesSkillInjector', () => {
expect(result.updated).toEqual(['webui-skill'])
expect(result.skipped).toEqual([])
await expect(readFile(join(hermesHome, 'skills', 'webui-skill', 'SKILL.md'), 'utf-8')).resolves.toBe('# WebUI Skill\n')
await expect(readManifest(join(hermesHome, 'skills'))).resolves.toMatchObject({ 'webui-skill': { owner: 'hermes-web-ui' } })
await expect(readManifest(join(hermesHome, 'skills'))).resolves.toMatchObject({ 'webui-skill': { owner: 'hermes-ui' } })
})
it('syncs bundled skills into default and named profiles without overwriting user-owned conflicts', async () => {
+10 -10
View File
@@ -129,7 +129,7 @@ describe('skills controller', () => {
})
it('lists configured external skill directories with external source while keeping local skills first', async () => {
const root = await mkdtemp(join(tmpdir(), 'hermes-web-ui-external-skills-'))
const root = await mkdtemp(join(tmpdir(), 'hermes-ui-external-skills-'))
const profileDir = join(root, 'profile')
const localSkillDir = join(profileDir, 'skills', 'tools', 'dupe-skill')
const externalDir = join(root, 'external-skills')
@@ -165,7 +165,7 @@ describe('skills controller', () => {
})
it('lists flat symlinked skills in the misc category', async () => {
const root = await mkdtemp(join(tmpdir(), 'hermes-web-ui-symlink-flat-skill-'))
const root = await mkdtemp(join(tmpdir(), 'hermes-ui-symlink-flat-skill-'))
const profileDir = join(root, 'profile')
const profileSkillsDir = join(profileDir, 'skills')
const sharedSkillDir = join(root, 'shared-skills', 'linked-flat-skill')
@@ -199,7 +199,7 @@ describe('skills controller', () => {
})
it('lists Codex user and system skills for the codex target', async () => {
const root = await mkdtemp(join(tmpdir(), 'hermes-web-ui-codex-skills-'))
const root = await mkdtemp(join(tmpdir(), 'hermes-ui-codex-skills-'))
const previousHome = process.env.HOME
const userSkillDir = join(root, '.agents', 'skills', 'user-skill')
const systemSkillDir = join(root, '.codex', 'skills', '.system', 'system-skill')
@@ -234,7 +234,7 @@ describe('skills controller', () => {
})
it('reads Codex system skill details for the codex target', async () => {
const root = await mkdtemp(join(tmpdir(), 'hermes-web-ui-codex-system-skill-'))
const root = await mkdtemp(join(tmpdir(), 'hermes-ui-codex-system-skill-'))
const previousHome = process.env.HOME
const systemSkillDir = join(root, '.codex', 'skills', '.system', 'imagegen')
@@ -278,7 +278,7 @@ describe('skills controller', () => {
})
it('traverses symlinked category entries without following hidden or cyclic links', async () => {
const root = await mkdtemp(join(tmpdir(), 'hermes-web-ui-symlink-category-skill-'))
const root = await mkdtemp(join(tmpdir(), 'hermes-ui-symlink-category-skill-'))
const profileDir = join(root, 'profile')
const toolsDir = join(profileDir, 'skills', 'tools')
const linkedSkillDir = join(root, 'shared-skills', 'linked-skill')
@@ -337,7 +337,7 @@ describe('skills controller', () => {
})
it('imports skills into the request-scoped profile directory', async () => {
const root = await mkdtemp(join(tmpdir(), 'hermes-web-ui-import-profile-'))
const root = await mkdtemp(join(tmpdir(), 'hermes-ui-import-profile-'))
const defaultProfileDir = join(root, 'default')
const researchProfileDir = join(root, 'research')
mockGetProfileDir.mockImplementation((profile: string) => profile === 'research' ? researchProfileDir : defaultProfileDir)
@@ -385,7 +385,7 @@ describe('skills controller', () => {
})
it('imports skills with valid encoded multipart filenames', async () => {
const root = await mkdtemp(join(tmpdir(), 'hermes-web-ui-import-encoded-filename-'))
const root = await mkdtemp(join(tmpdir(), 'hermes-ui-import-encoded-filename-'))
const profileDir = join(root, 'research')
mockGetProfileDir.mockReturnValue(profileDir)
@@ -412,7 +412,7 @@ describe('skills controller', () => {
})
it('deletes local skills only from the request-scoped profile directory', async () => {
const root = await mkdtemp(join(tmpdir(), 'hermes-web-ui-delete-profile-'))
const root = await mkdtemp(join(tmpdir(), 'hermes-ui-delete-profile-'))
const defaultProfileDir = join(root, 'default')
const researchProfileDir = join(root, 'research')
const defaultSkillDir = join(defaultProfileDir, 'skills', 'tools', 'dupe-skill')
@@ -443,7 +443,7 @@ describe('skills controller', () => {
})
it('updates local skill content in the request-scoped profile directory', async () => {
const root = await mkdtemp(join(tmpdir(), 'hermes-web-ui-update-skill-'))
const root = await mkdtemp(join(tmpdir(), 'hermes-ui-update-skill-'))
const defaultProfileDir = join(root, 'default')
const researchProfileDir = join(root, 'research')
const defaultSkillDir = join(defaultProfileDir, 'skills', 'tools', 'dupe-skill')
@@ -476,7 +476,7 @@ describe('skills controller', () => {
})
it('updates Codex user skills but not Codex system skills', async () => {
const root = await mkdtemp(join(tmpdir(), 'hermes-web-ui-update-codex-skill-'))
const root = await mkdtemp(join(tmpdir(), 'hermes-ui-update-codex-skill-'))
const previousHome = process.env.HOME
const userSkillDir = join(root, '.agents', 'skills', 'user-skill')
const systemSkillDir = join(root, '.codex', 'skills', '.system', 'system-skill')
+10 -10
View File
@@ -55,7 +55,7 @@ describe('studio MCP autoinject', () => {
const injectedDefault = await updateConfigYamlForProfileMock.mock.calls[0][1]({})
expect(injectedDefault.data.mcp_servers['hermes-studio-api']).toEqual({
command: process.execPath,
args: [join(process.cwd(), 'bin/hermes-studio-mcp.mjs'), 'api'],
args: [join(process.cwd(), 'bin/hermes-ui-mcp.mjs'), 'api'],
env: {
HERMES_WEB_UI_URL: 'http://127.0.0.1:8648',
HERMES_WEB_UI_HOME: '/Users/test/.hermes-web-ui',
@@ -69,7 +69,7 @@ describe('studio MCP autoinject', () => {
})
expect(injectedDefault.data.mcp_servers['hermes-studio-devices']).toMatchObject({
command: process.execPath,
args: [join(process.cwd(), 'bin/hermes-studio-mcp.mjs'), 'devices'],
args: [join(process.cwd(), 'bin/hermes-ui-mcp.mjs'), 'devices'],
env: {
HERMES_MCP_SERVER_NAME: 'hermes-studio-devices',
HERMES_MCP_TOOLSET: 'devices',
@@ -78,7 +78,7 @@ describe('studio MCP autoinject', () => {
})
expect(injectedDefault.data.mcp_servers['hermes-studio-use']).toMatchObject({
command: process.execPath,
args: [join(process.cwd(), 'bin/hermes-studio-mcp.mjs'), 'use'],
args: [join(process.cwd(), 'bin/hermes-ui-mcp.mjs'), 'use'],
env: {
HERMES_MCP_SERVER_NAME: 'hermes-studio-use',
HERMES_MCP_TOOLSET: 'use',
@@ -120,7 +120,7 @@ describe('studio MCP autoinject', () => {
mcp_servers: {
'hermes-studio-api': {
command: process.execPath,
args: [join(process.cwd(), 'bin/hermes-studio-mcp.mjs'), 'api'],
args: [join(process.cwd(), 'bin/hermes-ui-mcp.mjs'), 'api'],
env: {
HERMES_WEB_UI_URL: 'http://127.0.0.1:8648',
HERMES_WEB_UI_HOME: '/Users/test/.hermes-web-ui',
@@ -201,9 +201,9 @@ describe('studio MCP autoinject', () => {
expect(updated.data.mcp_servers['hermes-studio']).toBeUndefined()
expect(updated.data.mcp_servers['hermes-web-ui-mcp']).toBeUndefined()
expect(updated.data.mcp_servers['hermes-studio-api'].command).toBe(process.execPath)
expect(updated.data.mcp_servers['hermes-studio-api'].args).toEqual([join(process.cwd(), 'bin/hermes-studio-mcp.mjs'), 'api'])
expect(updated.data.mcp_servers['hermes-studio-devices'].args).toEqual([join(process.cwd(), 'bin/hermes-studio-mcp.mjs'), 'devices'])
expect(updated.data.mcp_servers['hermes-studio-use'].args).toEqual([join(process.cwd(), 'bin/hermes-studio-mcp.mjs'), 'use'])
expect(updated.data.mcp_servers['hermes-studio-api'].args).toEqual([join(process.cwd(), 'bin/hermes-ui-mcp.mjs'), 'api'])
expect(updated.data.mcp_servers['hermes-studio-devices'].args).toEqual([join(process.cwd(), 'bin/hermes-ui-mcp.mjs'), 'devices'])
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 () => {
@@ -216,13 +216,13 @@ describe('studio MCP autoinject', () => {
const injected = await updateConfigYamlForProfileMock.mock.calls[0][1]({})
expect(injected.data.mcp_servers['hermes-studio-api'].command).toBe('/runtime/node')
expect(injected.data.mcp_servers['hermes-studio-api'].args).toEqual([
join(process.cwd(), 'bin/hermes-studio-mcp.mjs'), 'api',
join(process.cwd(), 'bin/hermes-ui-mcp.mjs'), 'api',
])
expect(injected.data.mcp_servers['hermes-studio-devices'].args).toEqual([
join(process.cwd(), 'bin/hermes-studio-mcp.mjs'), 'devices',
join(process.cwd(), 'bin/hermes-ui-mcp.mjs'), 'devices',
])
expect(injected.data.mcp_servers['hermes-studio-use'].args).toEqual([
join(process.cwd(), 'bin/hermes-studio-mcp.mjs'), 'use',
join(process.cwd(), 'bin/hermes-ui-mcp.mjs'), 'use',
])
})
+11 -11
View File
@@ -22,9 +22,9 @@ async function loadUpdateController(overrides: LoadUpdateControllerOptions = {})
const spawn = overrides.spawn ?? vi.fn(() => ({ unref, on: vi.fn() }))
const existsSync = overrides.existsSync ?? vi.fn(() => true)
const readFileSync = overrides.readFileSync ?? vi.fn(() => JSON.stringify({
name: 'hermes-web-ui',
name: 'hermes-ui',
version: '0.0.0',
repository: { url: 'https://github.com/EKKOLearnAI/hermes-studio.git' },
repository: { url: 'https://gitea.erhe.top/toor/hermes-ui.git' },
}))
const appendFileSync = overrides.appendFileSync ?? vi.fn()
@@ -75,8 +75,8 @@ function getNpmCliPath() {
function getGlobalCliScript(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')
? join(prefix, 'node_modules', 'hermes-ui', 'bin', 'hermes-ui.mjs')
: join(prefix, 'lib', 'node_modules', 'hermes-ui', 'bin', 'hermes-ui.mjs')
}
describe('update controller', () => {
@@ -123,7 +123,7 @@ describe('update controller', () => {
expect(mocks.execFileSync).toHaveBeenCalledWith(
process.execPath,
[npmCli, 'install', '-g', 'hermes-web-ui@latest'],
[npmCli, 'install', '-g', 'hermes-ui@latest'],
expect.objectContaining({
encoding: 'utf-8',
timeout: 10 * 60 * 1000,
@@ -204,7 +204,7 @@ describe('update controller', () => {
it('returns a 500 with stderr when installation fails', async () => {
const execFileSync = vi.fn((_command: string, args: string[]) => {
if (args.includes('install') && args.includes('hermes-web-ui@latest')) {
if (args.includes('install') && args.includes('hermes-ui@latest')) {
const error = new Error('install failed') as Error & { stderr?: string }
error.stderr = 'engine mismatch'
throw error
@@ -239,7 +239,7 @@ describe('update controller', () => {
})
it('loads preview tags through async git with a short timeout', async () => {
process.env.HERMES_WEB_UI_PREVIEW_REPO = 'https://github.com/EKKOLearnAI/hermes-studio'
process.env.HERMES_WEB_UI_PREVIEW_REPO = 'https://gitea.erhe.top/toor/hermes-ui'
const execFile = vi.fn((_command: string, _args: string[], _options: any, callback: any) => {
callback(null, [
'ghi789\trefs/tags/v0.6.9',
@@ -278,14 +278,14 @@ describe('update controller', () => {
})
expect(mocks.execFile).toHaveBeenCalledWith(
'git',
['ls-remote', '--tags', '--refs', 'https://github.com/EKKOLearnAI/hermes-studio.git'],
['ls-remote', '--tags', '--refs', 'https://gitea.erhe.top/toor/hermes-ui.git'],
expect.objectContaining({ timeout: 8000 }),
expect.any(Function),
)
})
it('falls back to GitHub API when async git tag loading fails', async () => {
process.env.HERMES_WEB_UI_PREVIEW_REPO = 'https://github.com/EKKOLearnAI/hermes-studio'
process.env.HERMES_WEB_UI_PREVIEW_REPO = 'https://gitea.erhe.top/toor/hermes-ui'
const execFile = vi.fn((_command: string, _args: string[], _options: any, callback: any) => {
callback(new Error('git timeout'), '', '')
})
@@ -328,9 +328,9 @@ describe('update controller', () => {
],
})
expect(fetchMock).toHaveBeenCalledWith(
'https://api.github.com/repos/EKKOLearnAI/hermes-studio/tags?per_page=100',
'https://gitea.erhe.top/api/v1/repos/toor/hermes-ui/tags?per_page=100',
expect.objectContaining({
headers: { 'User-Agent': 'hermes-web-ui-preview' },
headers: { 'User-Agent': 'hermes-ui-preview' },
signal: expect.any(AbortSignal),
}),
)
+4 -4
View File
@@ -18,7 +18,7 @@ vi.mock('../../packages/server/src/services/hermes/hermes-profile', () => ({
}))
vi.mock('../../packages/server/src/services/hermes/upload-paths', () => ({
getProfileUploadDir: vi.fn((profile: string) => `/tmp/hermes-web-ui/upload/${profile}`),
getProfileUploadDir: vi.fn((profile: string) => `/tmp/hermes-ui/upload/${profile}`),
}))
function multipartBody(
@@ -62,10 +62,10 @@ describe('upload controller', () => {
await handleUpload(ctx)
expect(mkdirMock).toHaveBeenCalledWith('/tmp/hermes-web-ui/upload/research', { recursive: true })
expect(mkdirMock).toHaveBeenCalledWith('/tmp/hermes-ui/upload/research', { recursive: true })
expect(writeFileMock).toHaveBeenCalledOnce()
const [savedPath, data] = writeFileMock.mock.calls[0]
expect(normalizePath(savedPath)).toMatch(/^\/tmp\/hermes-web-ui\/upload\/research\/[a-f0-9]+\.txt$/)
expect(normalizePath(savedPath)).toMatch(/^\/tmp\/hermes-ui\/upload\/research\/[a-f0-9]+\.txt$/)
expect(data.toString('utf-8')).toBe('hello')
expect(ctx.body.files[0]).toMatchObject({ name: 'note.txt', path: savedPath })
})
@@ -87,7 +87,7 @@ describe('upload controller', () => {
expect(writeFileMock).toHaveBeenCalledOnce()
const [savedPath, data] = writeFileMock.mock.calls[0]
expect(normalizePath(savedPath)).toMatch(/^\/tmp\/hermes-web-ui\/upload\/research\/[a-f0-9]+\.txt$/)
expect(normalizePath(savedPath)).toMatch(/^\/tmp\/hermes-ui\/upload\/research\/[a-f0-9]+\.txt$/)
expect(data.toString('utf-8')).toBe('hello')
expect(ctx.body.files[0]).toMatchObject({ name: 'daily report.txt', path: savedPath })
})
+1 -1
View File
@@ -337,7 +337,7 @@ describe('user auth tables and middleware', () => {
username: 'admin',
role: 'super_admin',
type: 'access',
aud: 'hermes-web-ui',
aud: 'hermes-ui',
iat: 1,
exp: 3601,
})