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:
+164
-252
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { readFile, readdir } from 'node:fs/promises'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import path from 'node:path'
|
||||
@@ -114,8 +114,7 @@ for (const file of [
|
||||
for (const dir of [
|
||||
'packages/client/src',
|
||||
'packages/server/src',
|
||||
'packages/desktop',
|
||||
'packages/desktop/build/icons',
|
||||
'packages/skills',
|
||||
'tests/client',
|
||||
'tests/server',
|
||||
'tests/e2e',
|
||||
@@ -125,21 +124,6 @@ for (const dir of [
|
||||
requireDir(dir)
|
||||
}
|
||||
|
||||
for (const icon of [
|
||||
'packages/desktop/build/icon.png',
|
||||
'packages/desktop/build/icon.icns',
|
||||
'packages/desktop/build/icon.ico',
|
||||
'packages/desktop/build/icons/16x16.png',
|
||||
'packages/desktop/build/icons/32x32.png',
|
||||
'packages/desktop/build/icons/48x48.png',
|
||||
'packages/desktop/build/icons/64x64.png',
|
||||
'packages/desktop/build/icons/128x128.png',
|
||||
'packages/desktop/build/icons/256x256.png',
|
||||
'packages/desktop/build/icons/512x512.png',
|
||||
]) {
|
||||
requireFile(icon)
|
||||
}
|
||||
|
||||
const agents = await readText('AGENTS.md')
|
||||
const agentLines = agents.trimEnd().split(/\r?\n/)
|
||||
if (agentLines.length > 120) {
|
||||
@@ -176,9 +160,9 @@ const architecture = await readText('ARCHITECTURE.md')
|
||||
for (const phrase of [
|
||||
'packages/client/src',
|
||||
'packages/server/src',
|
||||
'packages/desktop',
|
||||
'packages/skills',
|
||||
'HERMES_WEB_UI_HOME',
|
||||
'fail_on_unmatched_files: true',
|
||||
'agent-bridge/python/',
|
||||
]) {
|
||||
if (!architecture.includes(phrase)) {
|
||||
fail(`ARCHITECTURE.md should document: ${phrase}`)
|
||||
@@ -243,247 +227,175 @@ for (const file of changedChatChainFragments) {
|
||||
}
|
||||
}
|
||||
|
||||
const desktopReleaseWorkflow = await readText('.github/workflows/desktop-release.yml')
|
||||
const desktopManualBuildWorkflow = await readText('.github/workflows/desktop-manual-build.yml')
|
||||
const desktopMacUpdateManifestWorkflow = await readText('.github/workflows/desktop-mac-update-manifest.yml')
|
||||
const desktopRuntimeWorkflow = await readText('.github/workflows/desktop-runtime.yml')
|
||||
const webuiReleaseWorkflow = await readText('.github/workflows/webui-release.yml')
|
||||
const dockerPublishWorkflow = await readText('.github/workflows/docker-publish.yml')
|
||||
const electronBuilderConfig = await readText('packages/desktop/electron-builder.yml')
|
||||
const desktopPackageJson = await readText('packages/desktop/package.json')
|
||||
const desktopInstallHermes = await readText('packages/desktop/scripts/install-hermes.mjs')
|
||||
const desktopHermesPatches = await readText('packages/desktop/scripts/apply-hermes-patches.mjs')
|
||||
const desktopWebuiServer = await readText('packages/desktop/src/main/webui-server.ts')
|
||||
const desktopMain = await readText('packages/desktop/src/main/index.ts')
|
||||
const desktopUpdater = await readText('packages/desktop/src/main/updater.ts')
|
||||
const desktopInstallerScript = await readText('packages/desktop/build/installer.nsh')
|
||||
const desktopRuntimeManager = await readText('packages/desktop/src/main/runtime-manager.ts')
|
||||
const desktopPaths = await readText('packages/desktop/src/main/paths.ts')
|
||||
const desktopRuntimeAssetName = await readText('packages/desktop/scripts/runtime-asset-name.mjs')
|
||||
if (!desktopReleaseWorkflow.includes('files: ${{ matrix.artifact_files }}')) {
|
||||
fail('desktop-release.yml must upload matrix-specific artifact_files')
|
||||
// --- Legacy brand scan -------------------------------------------------
|
||||
// The project was renamed from ekko / hermes-web-ui to hermes-ui. A few
|
||||
// legacy identifiers are deliberately preserved so existing deployments keep
|
||||
// working; see ALLOWED_LEGACY_BRAND below for the reason each one survives.
|
||||
const LEGACY_BRAND_PATTERNS = [/\bekko\b/i, /hermes-web-ui/i]
|
||||
|
||||
const SCAN_ROOTS = [
|
||||
'packages/client/src',
|
||||
'packages/server/src',
|
||||
'packages/skills',
|
||||
'tests',
|
||||
'scripts',
|
||||
'bin',
|
||||
'.github',
|
||||
]
|
||||
const SCAN_FILES = [
|
||||
'package.json',
|
||||
'Dockerfile',
|
||||
'docker-compose.yml',
|
||||
'AGENTS.md',
|
||||
'ARCHITECTURE.md',
|
||||
'DEVELOPMENT.md',
|
||||
'README.md',
|
||||
'README_zh.md',
|
||||
]
|
||||
const SCAN_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.mjs', '.cjs', '.vue', '.json', '.yml', '.yaml', '.md', '.py'])
|
||||
const SCAN_SKIP_DIRS = new Set(['node_modules', 'dist', '.git', 'reference_code', '__pycache__'])
|
||||
// This file necessarily spells out the patterns and allow-list entries it checks.
|
||||
const SCAN_SKIP_FILES = new Set(['scripts/harness-check.mjs'])
|
||||
|
||||
// Every entry must state why the legacy string is still allowed.
|
||||
const ALLOWED_LEGACY_BRAND = [
|
||||
{
|
||||
file: 'packages/server/src/db/index.ts',
|
||||
reason: 'DB filenames predate the rename; renamed files would orphan data in ~/.hermes-web-ui',
|
||||
},
|
||||
{
|
||||
file: 'bin/hermes-ui.mjs',
|
||||
reason: 'CLI reads the same pre-rename DB filename as packages/server/src/db/index.ts',
|
||||
},
|
||||
{
|
||||
file: 'packages/server/src/config.ts',
|
||||
reason: 'HERMES_WEB_UI_HOME / ~/.hermes-web-ui state directory is a preserved public contract',
|
||||
},
|
||||
{
|
||||
file: 'packages/server/src/middleware/user-auth.ts',
|
||||
reason: 'legacy JWT audience must stay accepted so upgrades do not force re-login',
|
||||
},
|
||||
{
|
||||
file: 'packages/server/src/services/hermes/skill-injector.ts',
|
||||
reason: 'legacy manifest owner must stay recognized so managed skills are not re-injected',
|
||||
},
|
||||
{
|
||||
file: 'packages/server/src/services/coding-agents.ts',
|
||||
reason: 'legacy MCP server/command names are detected to clean up old on-disk config',
|
||||
},
|
||||
{
|
||||
file: 'packages/server/src/services/hermes/studio-mcp-autoinject.ts',
|
||||
reason: 'legacy MCP server/command names are detected to clean up old on-disk config',
|
||||
},
|
||||
{
|
||||
file: 'tests/server/studio-mcp-autoinject.test.ts',
|
||||
reason: 'asserts legacy MCP entries are cleaned from existing on-disk config',
|
||||
},
|
||||
{
|
||||
file: 'tests/server/coding-agents-launch.test.ts',
|
||||
reason: 'asserts legacy MCP entries are cleaned from existing on-disk config',
|
||||
},
|
||||
{
|
||||
file: 'tests/client/markdown-rendering.test.ts',
|
||||
reason: 'fixture URL is arbitrary sample data, not project branding',
|
||||
},
|
||||
{
|
||||
file: 'tests/server/cli-port-detection.test.ts',
|
||||
reason: 'temp dir names and the preserved hermes-web-ui.db filename',
|
||||
},
|
||||
{
|
||||
file: 'tests/server/agent-bridge-python-concurrency.test.ts',
|
||||
reason: 'asserts preview socket path derived from the preview endpoint env var',
|
||||
},
|
||||
]
|
||||
|
||||
// Files whose remaining 'ekko' hits are the in-house agent runtime that Phase
|
||||
// 003 deletes outright. They are allow-listed here only until then; Phase 003
|
||||
// must delete these entries together with the files.
|
||||
const PHASE_003_EKKO_AGENT_FILES = [
|
||||
'packages/server/src/services/ekko-agent/manager.ts',
|
||||
'packages/server/src/services/ekko-agent/mcp.ts',
|
||||
'packages/server/src/services/hermes/run-chat/handle-ekko-agent-run.ts',
|
||||
'packages/server/src/services/hermes/run-chat/index.ts',
|
||||
'packages/server/src/services/hermes/run-chat/session-command.ts',
|
||||
'packages/server/src/services/hermes/run-chat/types.ts',
|
||||
'packages/client/src/api/coding-agents.ts',
|
||||
'packages/client/src/api/hermes/chat.ts',
|
||||
'packages/client/src/components/hermes/chat/ChatPanel.vue',
|
||||
'packages/client/src/components/hermes/chat/MessageList.vue',
|
||||
'packages/client/src/components/hermes/chat/SessionListItem.vue',
|
||||
'packages/client/src/stores/hermes/chat.ts',
|
||||
'packages/client/src/i18n/locales/en.ts',
|
||||
'packages/client/src/i18n/locales/zh.ts',
|
||||
'tests/ekko-agent',
|
||||
'tests/server/ekko-agent-manager.test.ts',
|
||||
'tests/server/ekko-agent-mcp.test.ts',
|
||||
'tests/server/run-chat-ekko-context.test.ts',
|
||||
'AGENTS.md',
|
||||
'ARCHITECTURE.md',
|
||||
]
|
||||
for (const target of PHASE_003_EKKO_AGENT_FILES) {
|
||||
ALLOWED_LEGACY_BRAND.push({
|
||||
file: target,
|
||||
reason: 'ekko-agent runtime is removed in Phase 003; this entry must be deleted then',
|
||||
})
|
||||
}
|
||||
|
||||
if (desktopReleaseWorkflow.includes('types: [published]')) {
|
||||
fail('desktop-release.yml must not run full desktop packaging on every published GitHub Release')
|
||||
}
|
||||
// Legacy identifiers that stay valid everywhere, because they are public
|
||||
// deployment contracts rather than project branding.
|
||||
const GLOBAL_ALLOWED_PATTERNS = [
|
||||
/HERMES_WEB_UI[A-Z0-9_]*/,
|
||||
/HERMES_WEBUI_STATE_DIR/,
|
||||
/\.hermes-web-ui/,
|
||||
/hermes_brightness/,
|
||||
/hermes_style/,
|
||||
/hermes_locale/,
|
||||
]
|
||||
|
||||
if (!desktopReleaseWorkflow.includes('gh release edit "$TAG" --repo "$GITHUB_REPOSITORY" --latest')) {
|
||||
fail('desktop-release.yml must mark successful full desktop releases as GitHub latest')
|
||||
}
|
||||
|
||||
for (const [file, text] of [
|
||||
['webui-release.yml', webuiReleaseWorkflow],
|
||||
['docker-publish.yml', dockerPublishWorkflow],
|
||||
]) {
|
||||
if (!text.includes('release:') || !text.includes('types: [published]')) {
|
||||
fail(`${file} must keep running on published GitHub Releases`)
|
||||
}
|
||||
if (!text.includes('gh release edit "$TAG" --repo "$GITHUB_REPOSITORY" --latest=false')) {
|
||||
fail(`${file} must keep published GitHub Releases out of latest`)
|
||||
}
|
||||
}
|
||||
|
||||
if (!webuiReleaseWorkflow.includes('make_latest: false')) {
|
||||
fail('webui-release.yml must not mark release uploads as GitHub latest')
|
||||
}
|
||||
|
||||
if (!electronBuilderConfig.includes('icon: build/icons')) {
|
||||
fail('electron-builder.yml must configure the Linux icon set')
|
||||
}
|
||||
|
||||
for (const target of ['target_os: darwin', 'target_os: win32', 'target_os: linux']) {
|
||||
if (!desktopReleaseWorkflow.includes(target)) {
|
||||
fail(`desktop-release.yml is missing matrix target ${target}`)
|
||||
}
|
||||
}
|
||||
|
||||
for (const expectedGlob of ['*.dmg', '*.exe', '*.AppImage']) {
|
||||
if (!desktopReleaseWorkflow.includes(expectedGlob)) {
|
||||
fail(`desktop-release.yml is missing expected artifact glob ${expectedGlob}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (!desktopReleaseWorkflow.includes('fail_on_unmatched_files: true')) {
|
||||
fail('desktop-release.yml must keep fail_on_unmatched_files: true')
|
||||
}
|
||||
|
||||
function workflowCaseBody(text, caseLabel) {
|
||||
const start = text.indexOf(`${caseLabel})`)
|
||||
if (start < 0) fail(`desktop-manual-build.yml is missing ${caseLabel} case`)
|
||||
const end = text.indexOf(';;', start)
|
||||
if (end < 0) fail(`desktop-manual-build.yml ${caseLabel} case is missing terminator`)
|
||||
return text.slice(start, end)
|
||||
}
|
||||
|
||||
for (const macCase of ['darwin-arm64', 'darwin-x64']) {
|
||||
const body = workflowCaseBody(desktopManualBuildWorkflow, macCase)
|
||||
if (body.includes('latest*.yml')) {
|
||||
fail(`desktop-manual-build.yml must not publish single-arch macOS update manifests from ${macCase}`)
|
||||
}
|
||||
for (const glob of ['*.dmg.blockmap', '*.zip.blockmap']) {
|
||||
if (!body.includes(glob)) {
|
||||
fail(`desktop-manual-build.yml ${macCase} must keep uploading ${glob}`)
|
||||
async function collectScanFiles(target, out) {
|
||||
const absolute = path.join(root, target)
|
||||
const stat = await readdir(absolute, { withFileTypes: true }).catch(() => null)
|
||||
if (!stat) return
|
||||
for (const entry of stat) {
|
||||
const relative = path.join(target, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
if (SCAN_SKIP_DIRS.has(entry.name)) continue
|
||||
await collectScanFiles(relative, out)
|
||||
} else if (SCAN_EXTENSIONS.has(path.extname(entry.name))) {
|
||||
out.push(relative)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const phrase of [
|
||||
'mac-update-manifest:',
|
||||
"if: needs.validate.outputs.target_os == 'darwin' && github.event.inputs.release_tag != ''",
|
||||
'Both macOS architectures are not available yet; leaving latest-mac.yml unchanged.',
|
||||
'gh release upload "$TAG" /tmp/latest-mac.yml',
|
||||
]) {
|
||||
if (!desktopManualBuildWorkflow.includes(phrase)) {
|
||||
fail(`desktop-manual-build.yml must include macOS manifest repair behavior: ${phrase}`)
|
||||
function isAllowedLegacyBrand(relativePath, line) {
|
||||
if (GLOBAL_ALLOWED_PATTERNS.some(pattern => pattern.test(line))) return true
|
||||
const normalized = relativePath.split(path.sep).join('/')
|
||||
// An allow-list entry may name a directory, in which case it covers its files.
|
||||
return ALLOWED_LEGACY_BRAND.some(
|
||||
entry => normalized === entry.file || normalized.startsWith(`${entry.file}/`),
|
||||
)
|
||||
}
|
||||
|
||||
const scanFiles = []
|
||||
for (const target of SCAN_ROOTS) await collectScanFiles(target, scanFiles)
|
||||
for (const file of SCAN_FILES) {
|
||||
if (existsSync(path.join(root, file))) scanFiles.push(file)
|
||||
}
|
||||
|
||||
const brandHits = []
|
||||
for (const relativePath of scanFiles) {
|
||||
if (SCAN_SKIP_FILES.has(relativePath.split(path.sep).join('/'))) continue
|
||||
const text = await readFile(path.join(root, relativePath), 'utf8')
|
||||
const lines = text.split(/\r?\n/)
|
||||
for (let index = 0; index < lines.length; index += 1) {
|
||||
const line = lines[index]
|
||||
if (!LEGACY_BRAND_PATTERNS.some(pattern => pattern.test(line))) continue
|
||||
if (isAllowedLegacyBrand(relativePath, line)) continue
|
||||
brandHits.push(`${relativePath}:${index + 1}: ${line.trim()}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (!desktopMacUpdateManifestWorkflow.includes('Repair macOS Update Manifest')) {
|
||||
fail('desktop-mac-update-manifest.yml must provide a manual macOS manifest repair workflow')
|
||||
}
|
||||
|
||||
if (!desktopMacUpdateManifestWorkflow.includes("gh release download \"$TAG\"") || !desktopMacUpdateManifestWorkflow.includes('/tmp/latest-mac.yml')) {
|
||||
fail('desktop-mac-update-manifest.yml must generate latest-mac.yml from release assets')
|
||||
}
|
||||
|
||||
if (!desktopMacUpdateManifestWorkflow.includes('gh release upload "$TAG" /tmp/latest-mac.yml')) {
|
||||
fail('desktop-mac-update-manifest.yml must upload the merged latest-mac.yml to the release')
|
||||
}
|
||||
|
||||
for (const phrase of [
|
||||
'resources/python/${os}-${arch}',
|
||||
'resources/node/${os}-${arch}',
|
||||
'resources/git/${os}-${arch}',
|
||||
]) {
|
||||
if (electronBuilderConfig.includes(phrase)) {
|
||||
fail(`electron-builder.yml must not bundle desktop runtime resource: ${phrase}`)
|
||||
}
|
||||
}
|
||||
|
||||
for (const phrase of [
|
||||
'"fetch:node"',
|
||||
'"fetch:git"',
|
||||
'"prepare:runtime"',
|
||||
'"package:runtime"',
|
||||
'"runtime:asset-name"',
|
||||
]) {
|
||||
if (!desktopPackageJson.includes(phrase)) {
|
||||
fail(`packages/desktop/package.json must support runtime package publishing: ${phrase}`)
|
||||
}
|
||||
}
|
||||
|
||||
for (const phrase of [
|
||||
'steps.check.outputs.missing',
|
||||
'npm --prefix packages/desktop run prepare:runtime',
|
||||
'npm --prefix packages/desktop run package:runtime',
|
||||
]) {
|
||||
if (!desktopRuntimeWorkflow.includes(phrase)) {
|
||||
fail(`desktop-runtime.yml must build and publish missing runtime package assets: ${phrase}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (!desktopRuntimeAssetName.includes('hermes-runtime-hermes-agent-')) {
|
||||
fail('runtime asset naming must include hermes-agent version')
|
||||
}
|
||||
|
||||
for (const phrase of [
|
||||
'websockets',
|
||||
'agent-browser@^0.26.0',
|
||||
'HERMES_CHROME_FOR_TESTING_VERSION',
|
||||
'149.0.7827.55',
|
||||
'pinChromeForTestingBundle',
|
||||
'chromeForTestingPlatform',
|
||||
'AGENT_BROWSER_HOME',
|
||||
'AGENT_BROWSER_EXECUTABLE_PATH',
|
||||
'PLAYWRIGHT_BROWSERS_PATH',
|
||||
'ms-playwright',
|
||||
'removeBrokenDashboardAuthPlugin',
|
||||
]) {
|
||||
if (!desktopInstallHermes.includes(phrase)) {
|
||||
fail(`install-hermes.mjs must bundle Hermes browser runtime support: ${phrase}`)
|
||||
}
|
||||
}
|
||||
|
||||
for (const phrase of [
|
||||
'from pathlib import Path',
|
||||
'browser stdout decode fallback is incomplete',
|
||||
'def _hermes_read_browser_output',
|
||||
'dingtalk AI Card webhook patches are incomplete',
|
||||
"plugins', 'platforms', 'dingtalk', 'adapter.py",
|
||||
"gateway', 'platforms', 'dingtalk.py",
|
||||
'sitecustomize hidden subprocess patch marker exists',
|
||||
'python compile check',
|
||||
]) {
|
||||
if (!desktopHermesPatches.includes(phrase)) {
|
||||
fail(`apply-hermes-patches.mjs must keep browser stdout fallback complete: ${phrase}`)
|
||||
}
|
||||
}
|
||||
|
||||
for (const phrase of [
|
||||
'bundledAgentBrowserHome',
|
||||
'AGENT_BROWSER_HOME',
|
||||
'bundledNodeBin',
|
||||
'HERMES_AGENT_NODE',
|
||||
'HERMES_AGENT_GIT',
|
||||
'PLAYWRIGHT_BROWSERS_PATH',
|
||||
'ms-playwright',
|
||||
]) {
|
||||
if (!desktopWebuiServer.includes(phrase)) {
|
||||
fail(`desktop webui server must expose bundled browser runtime: ${phrase}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (desktopWebuiServer.includes('bundledBrowserExecutable()')) {
|
||||
fail('desktop webui server must let agent-browser resolve the bundled browser from AGENT_BROWSER_HOME')
|
||||
}
|
||||
|
||||
for (const phrase of [
|
||||
'requestSingleInstanceLock(QUIT_EXISTING ? { quit: true } : undefined)',
|
||||
'hasQuitRequest(additionalData)',
|
||||
]) {
|
||||
if (!desktopMain.includes(phrase)) {
|
||||
fail(`desktop main process must forward --quit to an existing app instance: ${phrase}`)
|
||||
}
|
||||
}
|
||||
|
||||
for (const phrase of [
|
||||
'HERMES_STUDIO_EXE',
|
||||
'Get-CimInstance Win32_Process',
|
||||
'CloseMainWindow()',
|
||||
'Stop-Process -Id',
|
||||
]) {
|
||||
if (!desktopInstallerScript.includes(phrase)) {
|
||||
fail(`desktop installer must close stale Hermes Studio processes by installed executable path: ${phrase}`)
|
||||
}
|
||||
}
|
||||
|
||||
for (const phrase of [
|
||||
'https://download.ekkolearnai.com/latest',
|
||||
'https://github.com/EKKOLearnAI/hermes-studio/releases/latest/download',
|
||||
'checkForUpdatesWithFallback()',
|
||||
]) {
|
||||
if (!desktopUpdater.includes(phrase)) {
|
||||
fail(`desktop updater must check Cloudflare first and keep GitHub as fallback: ${phrase}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (desktopUpdater.includes('fetch(')) {
|
||||
fail('desktop updater must not make custom fetch requests to resolve the latest release tag')
|
||||
}
|
||||
|
||||
for (const phrase of [
|
||||
'HERMES_DESKTOP_RUNTIME_URL',
|
||||
'HERMES_DESKTOP_RUNTIME_BASE_URL',
|
||||
'runtime-manifest.json',
|
||||
]) {
|
||||
if (!desktopRuntimeManager.includes(phrase)) {
|
||||
fail(`desktop runtime manager must support downloadable runtime packages: ${phrase}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (!desktopPaths.includes('HERMES_DESKTOP_RUNTIME_DIR')) {
|
||||
fail('desktop paths must allow HERMES_DESKTOP_RUNTIME_DIR override')
|
||||
for (const hit of brandHits) {
|
||||
fail(`Legacy brand string must be removed (ekko / hermes-web-ui): ${hit}`)
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
|
||||
Reference in New Issue
Block a user