Phase 002: rebrand to hermes-ui and consolidate i18n to zh/en
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:
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
@@ -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',
|
||||
])
|
||||
})
|
||||
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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"')
|
||||
|
||||
@@ -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,
|
||||
@@ -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: '' })
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
@@ -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(() => ''),
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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',
|
||||
])
|
||||
})
|
||||
|
||||
|
||||
@@ -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),
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -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 })
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user