mirror of
https://github.com/EKKOLearnAI/hermes-web-ui.git
synced 2026-05-25 13:30:14 +00:00
fix: sync bundled skills across profiles
This commit is contained in:
+250
-108
@@ -36,10 +36,6 @@
|
||||
"name": "Files",
|
||||
"description": "Hermes file browser"
|
||||
},
|
||||
{
|
||||
"name": "Gateways",
|
||||
"description": "Gateway process management"
|
||||
},
|
||||
{
|
||||
"name": "Group Chat",
|
||||
"description": "Group chat management"
|
||||
@@ -875,6 +871,53 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/hermes/custom-model": {
|
||||
"put": {
|
||||
"tags": [
|
||||
"Models"
|
||||
],
|
||||
"summary": "Update custom-model",
|
||||
"description": "PUT /api/hermes/custom-model",
|
||||
"operationId": "addCustomModel",
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#/components/responses/BadRequest"
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/Unauthorized"
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Models"
|
||||
],
|
||||
"summary": "Delete custom-model",
|
||||
"description": "DELETE /api/hermes/custom-model",
|
||||
"operationId": "removeCustomModel",
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success"
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/Unauthorized"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/hermes/download": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -1132,110 +1175,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/hermes/gateways": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Gateways"
|
||||
],
|
||||
"summary": "List gateways",
|
||||
"description": "GET /api/hermes/gateways",
|
||||
"operationId": "list",
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success"
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/Unauthorized"
|
||||
},
|
||||
"404": {
|
||||
"description": "Not found"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/hermes/gateways/{name}/health": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Gateways"
|
||||
],
|
||||
"summary": "Get health",
|
||||
"description": "GET /api/hermes/gateways/:name/health",
|
||||
"operationId": "health",
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success"
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/Unauthorized"
|
||||
},
|
||||
"404": {
|
||||
"description": "Not found"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/hermes/gateways/{name}/start": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Gateways"
|
||||
],
|
||||
"summary": "Create start",
|
||||
"description": "POST /api/hermes/gateways/:name/start",
|
||||
"operationId": "start",
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#/components/responses/BadRequest"
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/Unauthorized"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/hermes/gateways/{name}/stop": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Gateways"
|
||||
],
|
||||
"summary": "Create stop",
|
||||
"description": "POST /api/hermes/gateways/:name/stop",
|
||||
"operationId": "stop",
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#/components/responses/BadRequest"
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/Unauthorized"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/hermes/group-chat/rooms": {
|
||||
"post": {
|
||||
"tags": [
|
||||
@@ -2117,6 +2056,32 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/hermes/profiles/runtime-statuses": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Profiles"
|
||||
],
|
||||
"summary": "Get runtime-statuses",
|
||||
"description": "GET /api/hermes/profiles/runtime-statuses",
|
||||
"operationId": "runtimeStatuses",
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success"
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/Unauthorized"
|
||||
},
|
||||
"404": {
|
||||
"description": "Not found"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/hermes/profiles/{name}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -2164,6 +2129,53 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/hermes/profiles/{name}/avatar": {
|
||||
"put": {
|
||||
"tags": [
|
||||
"Profiles"
|
||||
],
|
||||
"summary": "Update avatar",
|
||||
"description": "PUT /api/hermes/profiles/:name/avatar",
|
||||
"operationId": "updateAvatar",
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#/components/responses/BadRequest"
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/Unauthorized"
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Profiles"
|
||||
],
|
||||
"summary": "Delete avatar",
|
||||
"description": "DELETE /api/hermes/profiles/:name/avatar",
|
||||
"operationId": "deleteAvatar",
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success"
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/Unauthorized"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/hermes/profiles/{name}/export": {
|
||||
"post": {
|
||||
"tags": [
|
||||
@@ -2190,6 +2202,32 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/hermes/profiles/{name}/gateway/restart": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Profiles"
|
||||
],
|
||||
"summary": "Create restart",
|
||||
"description": "POST /api/hermes/profiles/:name/gateway/restart",
|
||||
"operationId": "restartGatewayForProfile",
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#/components/responses/BadRequest"
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/Unauthorized"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/hermes/profiles/{name}/rename": {
|
||||
"post": {
|
||||
"tags": [
|
||||
@@ -2216,6 +2254,84 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/hermes/profiles/{name}/restart": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Profiles"
|
||||
],
|
||||
"summary": "Create restart",
|
||||
"description": "POST /api/hermes/profiles/:name/restart",
|
||||
"operationId": "restartProfileRuntime",
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#/components/responses/BadRequest"
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/Unauthorized"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/hermes/profiles/{name}/runtime-status": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Profiles"
|
||||
],
|
||||
"summary": "Get runtime-status",
|
||||
"description": "GET /api/hermes/profiles/:name/runtime-status",
|
||||
"operationId": "runtimeStatus",
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success"
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/Unauthorized"
|
||||
},
|
||||
"404": {
|
||||
"description": "Not found"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/hermes/provider-models": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Models"
|
||||
],
|
||||
"summary": "Create provider-models",
|
||||
"description": "POST /api/hermes/provider-models",
|
||||
"operationId": "fetchProviderModelList",
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#/components/responses/BadRequest"
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/Unauthorized"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/hermes/search/sessions": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -2575,6 +2691,32 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/hermes/sessions/{id}/model": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Sessions"
|
||||
],
|
||||
"summary": "Create model",
|
||||
"description": "POST /api/hermes/sessions/:id/model",
|
||||
"operationId": "setModel",
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#/components/responses/BadRequest"
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/Unauthorized"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/hermes/sessions/{id}/rename": {
|
||||
"post": {
|
||||
"tags": [
|
||||
|
||||
@@ -14,6 +14,7 @@ import { logger } from '../../services/logger'
|
||||
import { smartCloneCleanup } from '../../services/hermes/profile-credentials'
|
||||
import { detectHermesRootHome } from '../../services/hermes/hermes-path'
|
||||
import { getActiveProfileName } from '../../services/hermes/hermes-profile'
|
||||
import { HermesSkillInjector } from '../../services/hermes/skill-injector'
|
||||
import type { HermesProfile } from '../../services/hermes/hermes-cli'
|
||||
|
||||
const bridgeCleanupClient = () => new AgentBridgeClient({ connectRetryMs: 0, timeoutMs: 5000 })
|
||||
@@ -89,6 +90,24 @@ function profileExistsForManualSwitch(name: string): boolean {
|
||||
return existsSync(join(base, 'profiles', name, 'config.yaml')) || existsSync(join(base, 'profiles', name))
|
||||
}
|
||||
|
||||
async function injectBundledSkillsForProfile(name: string): Promise<void> {
|
||||
try {
|
||||
const targetDir = HermesSkillInjector.resolveTargetDirForProfile(name)
|
||||
const result = await new HermesSkillInjector(undefined, targetDir).injectMissingSkills()
|
||||
const target = result.targets[0]
|
||||
if (target && (target.injected.length > 0 || target.updated.length > 0)) {
|
||||
logger.info({
|
||||
profile: name,
|
||||
targetDir,
|
||||
injected: target.injected,
|
||||
updated: target.updated,
|
||||
}, '[profiles] synced bundled skills for profile')
|
||||
}
|
||||
} catch (err: any) {
|
||||
logger.warn(err, '[profiles] failed to sync bundled skills for profile "%s"', name)
|
||||
}
|
||||
}
|
||||
|
||||
function deleteForbiddenProfileFromDisk(name: string): boolean {
|
||||
if (!isForbiddenProfileName(name)) return false
|
||||
const base = detectHermesRootHome()
|
||||
@@ -353,6 +372,8 @@ export async function create(ctx: any) {
|
||||
}
|
||||
}
|
||||
|
||||
await injectBundledSkillsForProfile(name)
|
||||
|
||||
ctx.body = {
|
||||
success: true,
|
||||
message: output.trim(),
|
||||
@@ -625,6 +646,8 @@ export async function switchProfile(ctx: any) {
|
||||
logger.error(err, 'Ensure config failed')
|
||||
}
|
||||
|
||||
await injectBundledSkillsForProfile(name)
|
||||
|
||||
// TODO: re-enable pending session delete drain after confirming safety
|
||||
// const drainResult = await SessionDeleter.getInstance().drain(name)
|
||||
SessionDeleter.getInstance().switchProfile(name)
|
||||
|
||||
@@ -92,10 +92,16 @@ export async function bootstrap() {
|
||||
const skillInjector = new HermesSkillInjector()
|
||||
const injectionResult = await skillInjector.injectMissingSkills()
|
||||
if (injectionResult.injected.length > 0) {
|
||||
console.log('[bootstrap] bundled skills injected:', injectionResult.injected.join(', '))
|
||||
logger.info({
|
||||
injected: [...new Set(injectionResult.injected)],
|
||||
targetCount: injectionResult.targets.length,
|
||||
}, '[bootstrap] bundled skills injected')
|
||||
}
|
||||
if (injectionResult.updated.length > 0) {
|
||||
console.log('[bootstrap] bundled skills updated:', injectionResult.updated.join(', '))
|
||||
logger.info({
|
||||
updated: [...new Set(injectionResult.updated)],
|
||||
targetCount: injectionResult.targets.length,
|
||||
}, '[bootstrap] bundled skills updated')
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn(err, '[bootstrap] failed to inject bundled skills')
|
||||
|
||||
@@ -1,22 +1,32 @@
|
||||
import { copyFile, mkdir, readdir, rm, stat } from 'fs/promises'
|
||||
import { existsSync } from 'fs'
|
||||
import { existsSync, readdirSync } from 'fs'
|
||||
import { join, resolve } from 'path'
|
||||
import { getActiveProfileDir } from './hermes-profile'
|
||||
import { detectHermesRootHome } from './hermes-path'
|
||||
import { logger } from '../logger'
|
||||
|
||||
export interface SkillInjectionResult {
|
||||
sourceDir: string
|
||||
export interface SkillInjectionTargetResult {
|
||||
profile?: string
|
||||
targetDir: string
|
||||
injected: string[]
|
||||
updated: string[]
|
||||
skipped: string[]
|
||||
}
|
||||
|
||||
export interface SkillInjectionResult extends SkillInjectionTargetResult {
|
||||
sourceDir: string
|
||||
targets: SkillInjectionTargetResult[]
|
||||
}
|
||||
|
||||
export class HermesSkillInjector {
|
||||
private readonly targetDirs: string[]
|
||||
|
||||
constructor(
|
||||
private readonly sourceDir = HermesSkillInjector.resolveSourceDir(),
|
||||
private readonly targetDir = join(getActiveProfileDir(), 'skills'),
|
||||
) {}
|
||||
targetDirOrDirs: string | string[] = HermesSkillInjector.resolveTargetDirs(),
|
||||
) {
|
||||
const targetDirs = Array.isArray(targetDirOrDirs) ? targetDirOrDirs : [targetDirOrDirs]
|
||||
this.targetDirs = [...new Set(targetDirs.map(targetDir => resolve(targetDir)))]
|
||||
}
|
||||
|
||||
static resolveSourceDir(env: NodeJS.ProcessEnv = process.env, baseDir = __dirname): string {
|
||||
const override = env.HERMES_WEB_UI_SKILLS_DIR?.trim()
|
||||
@@ -34,13 +44,52 @@ export class HermesSkillInjector {
|
||||
return candidates.find(candidate => existsSync(candidate)) || candidates[0]
|
||||
}
|
||||
|
||||
static resolveTargetDirs(rootDir = detectHermesRootHome()): string[] {
|
||||
const root = resolve(rootDir)
|
||||
const targetDirs = [join(root, 'skills')]
|
||||
const profilesDir = join(root, 'profiles')
|
||||
|
||||
try {
|
||||
const entries = readdirSync(profilesDir, { withFileTypes: true })
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory() && entry.name.trim() && !entry.name.startsWith('.')) {
|
||||
targetDirs.push(join(profilesDir, entry.name, 'skills'))
|
||||
}
|
||||
}
|
||||
} catch { /* no named profiles */ }
|
||||
|
||||
return [...new Set(targetDirs.map(targetDir => resolve(targetDir)))]
|
||||
}
|
||||
|
||||
static resolveTargetDirForProfile(profile: string, rootDir = detectHermesRootHome()): string {
|
||||
const name = String(profile || '').trim()
|
||||
const root = resolve(rootDir)
|
||||
if (!name || name === 'default') return join(root, 'skills')
|
||||
return join(root, 'profiles', name, 'skills')
|
||||
}
|
||||
|
||||
private static profileForTargetDir(targetDir: string, rootDir = detectHermesRootHome()): string {
|
||||
const root = resolve(rootDir)
|
||||
const target = resolve(targetDir)
|
||||
if (target === resolve(join(root, 'skills'))) return 'default'
|
||||
|
||||
const profilesRoot = resolve(join(root, 'profiles'))
|
||||
const relativeToProfiles = target.startsWith(profilesRoot)
|
||||
? target.slice(profilesRoot.length).replace(/^[/\\]+/, '')
|
||||
: ''
|
||||
const [profileName, skillsSegment] = relativeToProfiles.split(/[/\\]+/)
|
||||
return profileName && skillsSegment === 'skills' ? profileName : 'unknown'
|
||||
}
|
||||
|
||||
async injectMissingSkills(): Promise<SkillInjectionResult> {
|
||||
const result: SkillInjectionResult = {
|
||||
sourceDir: this.sourceDir,
|
||||
targetDir: this.targetDir,
|
||||
targetDir: this.targetDirs[0] || '',
|
||||
injected: [],
|
||||
updated: [],
|
||||
skipped: [],
|
||||
targets: [],
|
||||
}
|
||||
|
||||
if (!await this.isDirectory(this.sourceDir)) {
|
||||
@@ -48,12 +97,53 @@ export class HermesSkillInjector {
|
||||
return result
|
||||
}
|
||||
|
||||
await mkdir(this.targetDir, { recursive: true })
|
||||
const entries = await readdir(this.sourceDir, { withFileTypes: true })
|
||||
const bundledSkillNames = entries
|
||||
.filter(entry => entry.isDirectory() && !entry.name.startsWith('.'))
|
||||
.map(entry => entry.name)
|
||||
|
||||
logger.info({
|
||||
sourceDir: this.sourceDir,
|
||||
targetDirs: this.targetDirs,
|
||||
targetCount: this.targetDirs.length,
|
||||
bundledSkillNames,
|
||||
}, '[skill-injector] syncing bundled skills across profiles')
|
||||
|
||||
for (const targetDir of this.targetDirs) {
|
||||
const targetResult = await this.injectIntoTarget(targetDir, entries)
|
||||
result.targets.push(targetResult)
|
||||
result.injected.push(...targetResult.injected)
|
||||
result.updated.push(...targetResult.updated)
|
||||
result.skipped.push(...targetResult.skipped)
|
||||
}
|
||||
|
||||
logger.info({
|
||||
sourceDir: this.sourceDir,
|
||||
targetCount: result.targets.length,
|
||||
injected: [...new Set(result.injected)],
|
||||
updated: [...new Set(result.updated)],
|
||||
skipped: [...new Set(result.skipped)],
|
||||
targets: result.targets,
|
||||
}, '[skill-injector] completed bundled skills sync')
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private async injectIntoTarget(targetDir: string, entries: import('fs').Dirent[]): Promise<SkillInjectionTargetResult> {
|
||||
const profile = HermesSkillInjector.profileForTargetDir(targetDir)
|
||||
const result: SkillInjectionTargetResult = {
|
||||
profile,
|
||||
targetDir,
|
||||
injected: [],
|
||||
updated: [],
|
||||
skipped: [],
|
||||
}
|
||||
|
||||
await mkdir(targetDir, { recursive: true })
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory() || entry.name.startsWith('.')) continue
|
||||
const sourceSkillDir = join(this.sourceDir, entry.name)
|
||||
const targetSkillDir = join(this.targetDir, entry.name)
|
||||
const targetSkillDir = join(targetDir, entry.name)
|
||||
const existed = existsSync(targetSkillDir)
|
||||
if (existsSync(targetSkillDir)) {
|
||||
await rm(targetSkillDir, { recursive: true, force: true })
|
||||
@@ -65,9 +155,10 @@ export class HermesSkillInjector {
|
||||
|
||||
if (result.injected.length > 0 || result.updated.length > 0) {
|
||||
logger.info({
|
||||
profile,
|
||||
injected: result.injected,
|
||||
updated: result.updated,
|
||||
targetDir: this.targetDir,
|
||||
targetDir,
|
||||
}, '[skill-injector] synced bundled skills')
|
||||
}
|
||||
return result
|
||||
|
||||
@@ -61,4 +61,41 @@ describe('HermesSkillInjector', () => {
|
||||
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('# Bundled Existing\n')
|
||||
})
|
||||
|
||||
it('syncs bundled skills into default and named profiles only touching bundled names', async () => {
|
||||
const source = await tempDir('hermes-skill-source-')
|
||||
const hermesHome = await tempDir('hermes-skill-home-')
|
||||
process.env.HERMES_HOME = hermesHome
|
||||
|
||||
await mkdir(join(source, 'webui-skill'), { recursive: true })
|
||||
await writeFile(join(source, 'webui-skill', 'SKILL.md'), '# WebUI Skill\n', 'utf-8')
|
||||
|
||||
await mkdir(join(hermesHome, 'skills', 'webui-skill'), { recursive: true })
|
||||
await writeFile(join(hermesHome, 'skills', 'webui-skill', 'SKILL.md'), '# Old WebUI Skill\n', 'utf-8')
|
||||
await mkdir(join(hermesHome, 'skills', 'local-skill'), { recursive: true })
|
||||
await writeFile(join(hermesHome, 'skills', 'local-skill', 'SKILL.md'), '# Local Skill\n', 'utf-8')
|
||||
|
||||
await mkdir(join(hermesHome, 'profiles', 'alpha', 'skills'), { recursive: true })
|
||||
await mkdir(join(hermesHome, 'profiles', 'beta', 'skills', 'webui-skill'), { recursive: true })
|
||||
await writeFile(join(hermesHome, 'profiles', 'beta', 'skills', 'webui-skill', 'SKILL.md'), '# Old Profile Skill\n', 'utf-8')
|
||||
await mkdir(join(hermesHome, 'profiles', 'beta', 'skills', 'profile-local'), { recursive: true })
|
||||
await writeFile(join(hermesHome, 'profiles', 'beta', 'skills', 'profile-local', 'SKILL.md'), '# Profile Local\n', 'utf-8')
|
||||
|
||||
const { HermesSkillInjector } = await import('../../packages/server/src/services/hermes/skill-injector')
|
||||
const result = await new HermesSkillInjector(source).injectMissingSkills()
|
||||
|
||||
expect(result.targets.map(target => target.targetDir)).toEqual([
|
||||
join(hermesHome, 'skills'),
|
||||
join(hermesHome, 'profiles', 'alpha', 'skills'),
|
||||
join(hermesHome, 'profiles', 'beta', 'skills'),
|
||||
])
|
||||
expect(result.injected).toEqual(['webui-skill'])
|
||||
expect(result.updated).toEqual(['webui-skill', 'webui-skill'])
|
||||
|
||||
await expect(readFile(join(hermesHome, 'skills', 'webui-skill', 'SKILL.md'), 'utf-8')).resolves.toBe('# WebUI Skill\n')
|
||||
await expect(readFile(join(hermesHome, 'profiles', 'alpha', 'skills', 'webui-skill', 'SKILL.md'), 'utf-8')).resolves.toBe('# WebUI Skill\n')
|
||||
await expect(readFile(join(hermesHome, 'profiles', 'beta', 'skills', 'webui-skill', 'SKILL.md'), 'utf-8')).resolves.toBe('# WebUI Skill\n')
|
||||
await expect(readFile(join(hermesHome, 'skills', 'local-skill', 'SKILL.md'), 'utf-8')).resolves.toBe('# Local Skill\n')
|
||||
await expect(readFile(join(hermesHome, 'profiles', 'beta', 'skills', 'profile-local', 'SKILL.md'), 'utf-8')).resolves.toBe('# Profile Local\n')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user