feat(skills): enable Hermes in unified Skills management

Wire hermes through SkillApps struct, DAO SQL, command parser, and
SKILLS_APP_IDS. Add a Skills entry to the Hermes toolbar. Simplify
skill_sync test fixtures to use SkillApps::default().
This commit is contained in:
Jason
2026-04-20 22:40:25 +08:00
parent 0be75668cc
commit 1227e29d8b
6 changed files with 40 additions and 34 deletions
+8 -3
View File
@@ -82,6 +82,8 @@ pub struct SkillApps {
pub gemini: bool,
#[serde(default)]
pub opencode: bool,
#[serde(default)]
pub hermes: bool,
}
impl SkillApps {
@@ -92,8 +94,8 @@ impl SkillApps {
AppType::Codex => self.codex,
AppType::Gemini => self.gemini,
AppType::OpenCode => self.opencode,
AppType::Hermes => self.hermes,
AppType::OpenClaw => false, // OpenClaw doesn't support Skills
AppType::Hermes => false, // Hermes doesn't support Skills yet
}
}
@@ -104,8 +106,8 @@ impl SkillApps {
AppType::Codex => self.codex = enabled,
AppType::Gemini => self.gemini = enabled,
AppType::OpenCode => self.opencode = enabled,
AppType::Hermes => self.hermes = enabled,
AppType::OpenClaw => {} // OpenClaw doesn't support Skills, ignore
AppType::Hermes => {} // Hermes doesn't support Skills yet, ignore
}
}
@@ -124,12 +126,15 @@ impl SkillApps {
if self.opencode {
apps.push(AppType::OpenCode);
}
if self.hermes {
apps.push(AppType::Hermes);
}
apps
}
/// 检查是否所有应用都未启用
pub fn is_empty(&self) -> bool {
!self.claude && !self.codex && !self.gemini && !self.opencode
!self.claude && !self.codex && !self.gemini && !self.opencode && !self.hermes
}
/// 仅启用指定应用(其他应用设为禁用)
+1
View File
@@ -25,6 +25,7 @@ fn parse_app_type(app: &str) -> Result<AppType, String> {
"codex" => Ok(AppType::Codex),
"gemini" => Ok(AppType::Gemini),
"opencode" => Ok(AppType::OpenCode),
"hermes" => Ok(AppType::Hermes),
_ => Err(format!("不支持的 app 类型: {app}")),
}
}
+14 -11
View File
@@ -23,7 +23,7 @@ impl Database {
.prepare(
"SELECT id, name, description, directory, repo_owner, repo_name, repo_branch,
readme_url, enabled_claude, enabled_codex, enabled_gemini, enabled_opencode,
installed_at, content_hash, updated_at
enabled_hermes, installed_at, content_hash, updated_at
FROM skills ORDER BY name ASC",
)
.map_err(|e| AppError::Database(e.to_string()))?;
@@ -44,10 +44,11 @@ impl Database {
codex: row.get(9)?,
gemini: row.get(10)?,
opencode: row.get(11)?,
hermes: row.get(12)?,
},
installed_at: row.get(12)?,
content_hash: row.get(13)?,
updated_at: row.get::<_, i64>(14).unwrap_or(0),
installed_at: row.get(13)?,
content_hash: row.get(14)?,
updated_at: row.get::<_, i64>(15).unwrap_or(0),
})
})
.map_err(|e| AppError::Database(e.to_string()))?;
@@ -67,7 +68,7 @@ impl Database {
.prepare(
"SELECT id, name, description, directory, repo_owner, repo_name, repo_branch,
readme_url, enabled_claude, enabled_codex, enabled_gemini, enabled_opencode,
installed_at, content_hash, updated_at
enabled_hermes, installed_at, content_hash, updated_at
FROM skills WHERE id = ?1",
)
.map_err(|e| AppError::Database(e.to_string()))?;
@@ -87,10 +88,11 @@ impl Database {
codex: row.get(9)?,
gemini: row.get(10)?,
opencode: row.get(11)?,
hermes: row.get(12)?,
},
installed_at: row.get(12)?,
content_hash: row.get(13)?,
updated_at: row.get::<_, i64>(14).unwrap_or(0),
installed_at: row.get(13)?,
content_hash: row.get(14)?,
updated_at: row.get::<_, i64>(15).unwrap_or(0),
})
});
@@ -109,7 +111,7 @@ impl Database {
(id, name, description, directory, repo_owner, repo_name, repo_branch,
readme_url, enabled_claude, enabled_codex, enabled_gemini, enabled_opencode, enabled_hermes,
installed_at, content_hash, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, 0, ?13, ?14, ?15)",
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16)",
params![
skill.id,
skill.name,
@@ -123,6 +125,7 @@ impl Database {
skill.apps.codex,
skill.apps.gemini,
skill.apps.opencode,
skill.apps.hermes,
skill.installed_at,
skill.content_hash,
skill.updated_at,
@@ -154,8 +157,8 @@ impl Database {
let conn = lock_conn!(self.conn);
let affected = conn
.execute(
"UPDATE skills SET enabled_claude = ?1, enabled_codex = ?2, enabled_gemini = ?3, enabled_opencode = ?4 WHERE id = ?5",
params![apps.claude, apps.codex, apps.gemini, apps.opencode, id],
"UPDATE skills SET enabled_claude = ?1, enabled_codex = ?2, enabled_gemini = ?3, enabled_opencode = ?4, enabled_hermes = ?5 WHERE id = ?6",
params![apps.claude, apps.codex, apps.gemini, apps.opencode, apps.hermes, id],
)
.map_err(|e| AppError::Database(e.to_string()))?;
Ok(affected > 0)
+5 -18
View File
@@ -53,10 +53,8 @@ fn import_from_apps_respects_explicit_app_selection() {
vec![ImportSkillSelection {
directory: "shared-skill".to_string(),
apps: SkillApps {
claude: false,
codex: false,
gemini: false,
opencode: true,
..Default::default()
},
}],
)
@@ -103,12 +101,7 @@ fn sync_to_app_removes_disabled_and_orphaned_ssot_symlinks() {
repo_name: None,
repo_branch: None,
readme_url: None,
apps: SkillApps {
claude: false,
codex: false,
gemini: false,
opencode: false,
},
apps: SkillApps::default(),
installed_at: 0,
content_hash: None,
updated_at: 0,
@@ -151,9 +144,7 @@ fn uninstall_skill_creates_backup_before_removing_ssot() {
readme_url: None,
apps: SkillApps {
claude: true,
codex: false,
gemini: false,
opencode: false,
..Default::default()
},
installed_at: 123,
content_hash: None,
@@ -221,9 +212,7 @@ fn restore_skill_backup_restores_files_to_ssot_and_current_app() {
readme_url: None,
apps: SkillApps {
claude: true,
codex: false,
gemini: false,
opencode: false,
..Default::default()
},
installed_at: 456,
content_hash: None,
@@ -304,9 +293,7 @@ fn delete_skill_backup_removes_backup_directory() {
readme_url: None,
apps: SkillApps {
claude: true,
codex: false,
gemini: false,
opencode: false,
..Default::default()
},
installed_at: 789,
content_hash: None,
+9
View File
@@ -1388,6 +1388,15 @@ function App() {
>
{activeApp === "hermes" ? (
<>
<Button
variant="ghost"
size="sm"
onClick={() => setCurrentView("skills")}
className="text-muted-foreground hover:text-foreground hover:bg-black/5 dark:hover:bg-white/5"
title={t("skills.manage")}
>
<Wrench className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="sm"
+3 -2
View File
@@ -24,16 +24,17 @@ export const APP_IDS: AppId[] = [
"hermes",
];
/** App IDs shown in Skills panels (excludes OpenClaw and Hermes — neither supports Skills) */
/** App IDs shown in Skills panels (excludes OpenClaw — it doesn't support Skills) */
export const SKILLS_APP_IDS: AppId[] = [
"claude",
"codex",
"gemini",
"opencode",
"hermes",
];
/** App IDs shown in MCP panels (excludes OpenClaw) */
export const MCP_APP_IDS: AppId[] = [...SKILLS_APP_IDS, "hermes"];
export const MCP_APP_IDS: AppId[] = [...SKILLS_APP_IDS];
export const APP_ICON_MAP: Record<AppId, AppConfig> = {
claude: {