fix(install): 支持 Hermes 多 profile,避免 skill 装错位置 (#102)

install.sh / install.ps1 原先只把 Hermes skill 装到根 <base>/skills,
多 profile 环境下 profiles/<name>/skills 完全不会被写入,导致静默错装。

- 新增 --profile / -Profile 指定目标 profile → <base>/profiles/<name>/skills
  (PowerShell 用 $HermesProfile + [Alias('Profile')] 规避自动变量 $Profile 遮蔽)
- 存在 profile 目录但未指定时报错退出并列出现有 profile,不再静默装到根目录
- <base> 统一解析 HERMES_HOME > $LOCALAPPDATA/hermes > ~/.hermes,detect 同步识别
- all 模式下单个工具失败不再中断其余安装,按失败数返回非零退出码
- 补充 --help / 用法说明;PowerShell 端 Join-Path 跨平台化

Closes #102

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
AI不止语
2026-07-30 00:19:50 +08:00
parent 2ecfabf8e9
commit d475a81e3e
2 changed files with 159 additions and 25 deletions
+76 -7
View File
@@ -4,7 +4,7 @@
# 请先运行 scripts\convert.ps1 生成集成文件。
#
# 用法:
# .\scripts\install.ps1 [-Tool <名称>] [-Help]
# .\scripts\install.ps1 [-Tool <名称>] [-Profile <名称>] [-Help]
#
# 支持的工具:
# claude-code — 复制到 %USERPROFILE%\.claude\agents\
@@ -30,10 +30,19 @@
# Discord 模式下 Hermes 会把每个 skill 注册为斜杠命令,
# 总 JSON 超过 8000 字符会被 Discord API 拒绝 (error 50035)
# 若需要在 Discord 中使用建议按分类分批安装。
# -Profile <名称> 多 profile 环境下指定安装到哪个 profileissue #102)。
# Hermes 每个 profile 有独立 skill 库,路径为
# <base>\profiles\<名称>\skills\
# 其中 <base> = HERMES_HOME > $LOCALAPPDATA\hermes > ~\.hermes。
# 未指定 -Profile 却检测到已存在 profile 目录时会报错退出,
# 避免静默装到根目录。例如:-Tool hermes -Profile work
param(
[string]$Tool = "all",
[string[]]$Category = @(),
# 用 $HermesProfile 而非 $Profile$Profile 是 PowerShell 自动变量,避免遮蔽(issue #102)
[Alias('Profile')]
[string]$HermesProfile = "",
[switch]$Help
)
@@ -60,7 +69,7 @@ function Write-Dim { param($msg) Write-Host " $msg" -ForegroundColor Dar
# --- 用法 ---
if ($Help) {
Get-Content $MyInvocation.MyCommand.Path | Select-Object -Skip 2 -First 22 |
Get-Content $MyInvocation.MyCommand.Path | Select-Object -Skip 2 -First 36 |
ForEach-Object { $_ -replace '^# ?','' }
exit 0
}
@@ -103,7 +112,9 @@ function Detect-Tool {
"workbuddy" { (Get-Command workbuddy -ErrorAction SilentlyContinue) -or
(Test-Path (Join-Path $Home_ ".workbuddy")) }
"hermes" { (Get-Command hermes -ErrorAction SilentlyContinue) -or
(Test-Path (Join-Path $Home_ ".hermes")) }
(Test-Path (Join-Path $Home_ ".hermes")) -or
($env:HERMES_HOME -and (Test-Path $env:HERMES_HOME)) -or
($env:LOCALAPPDATA -and (Test-Path (Join-Path $env:LOCALAPPDATA "hermes"))) }
"kiro" { (Get-Command kiro -ErrorAction SilentlyContinue) -or
(Get-Command kiro-cli -ErrorAction SilentlyContinue) -or
(Test-Path (Join-Path $Home_ ".kiro")) }
@@ -339,10 +350,53 @@ function Install-WorkBuddy {
Write-OK "WorkBuddy: $count 个 skills -> $dest"
}
# Hermes 安装根目录(不含 profile):HERMES_HOME > $LOCALAPPDATA\hermes > ~\.hermesissue #82/#102
function Get-HermesBaseDir {
if ($env:HERMES_HOME) { return $env:HERMES_HOME }
if ($env:LOCALAPPDATA -and (Test-Path (Join-Path $env:LOCALAPPDATA "hermes"))) {
return (Join-Path $env:LOCALAPPDATA "hermes")
}
return (Join-Path $Home_ ".hermes")
}
# 现有 profile 名称(<base>\profiles\<name>\
function Get-HermesProfileNames {
param($base)
$pdir = Join-Path $base "profiles"
if (-not (Test-Path $pdir)) { return @() }
@(Get-ChildItem -Path $pdir -Directory -ErrorAction SilentlyContinue | ForEach-Object Name)
}
function Install-Hermes {
$src = Join-Path $Integrations "hermes"
$dest = Join-Path $Home_ ".hermes\skills"
if (-not (Test-Path $src)) { Write-Err "integrations\hermes 不存在,请先运行 convert.ps1 -Tool hermes"; return }
if (-not (Test-Path $src)) { Write-Err "integrations\hermes 不存在,请先运行 convert.ps1 -Tool hermes"; $script:FailedTools += "hermes"; return }
# 安装目录解析(issue #82 / #102):
# 1. -Profile <name> -> <base>\profiles\<name>\skills
# 2. 存在 profile 目录但未指定 -Profile -> 报错退出(避免静默装到根目录)
# 3. 默认 -> <base>\skills
$base = Get-HermesBaseDir
$profileNote = ""
if ($HermesProfile) {
$profileDir = Join-Path (Join-Path $base "profiles") $HermesProfile
$dest = Join-Path $profileDir "skills"
$profileNote = " [profile: $HermesProfile]"
if (-not (Test-Path $profileDir)) {
$existing = (Get-HermesProfileNames $base) -join ", "
Write-Warn "profile '$HermesProfile' 尚不存在,将新建目录 $base\profiles\$HermesProfile\。"
Write-Warn "请确认名称无误(现有 profile: $existing)。"
}
} else {
$names = @(Get-HermesProfileNames $base)
if ($names.Count -gt 0) {
Write-Err "检测到 Hermes profile 目录($base\profiles\),但未指定 -Profile。"
Write-Err "为避免装错位置,请用 -Profile <名称> 指定目标 profile。"
Write-Err "现有 profile: $($names -join ', ')"
Write-Err "或设置 HERMES_HOME 指向单一 profile 根目录后重试。"
$script:FailedTools += "hermes"
return
}
$dest = Join-Path $base "skills"
}
$filterNote = ""
if ($Category.Count -gt 0) {
@@ -350,6 +404,7 @@ function Install-Hermes {
if (-not (Test-Path (Join-Path $src $c))) {
$avail = (Get-ChildItem -Path $src -Directory | ForEach-Object Name) -join ", "
Write-Err "hermes 分类不存在: $c(可选: $avail"
$script:FailedTools += "hermes"
return
}
}
@@ -371,7 +426,7 @@ function Install-Hermes {
}
}
}
Write-OK "Hermes Agent: $count 个 skills -> $dest$filterNote"
Write-OK "Hermes Agent: $count 个 skills -> $dest$filterNote$profileNote"
if ($Category.Count -eq 0 -and $count -gt 80) {
Write-Warn "Hermes Discord 模式对斜杠命令总长有 8000 字符上限(error 50035)。"
Write-Warn "若要在 Discord 中使用,建议用 -Category <名称> 按分类分批安装。"
@@ -420,6 +475,10 @@ if ($Category.Count -gt 0 -and $Tool -ne "hermes") {
Write-Warn "-Category 仅对 -Tool hermes 生效,已忽略。"
$Category = @()
}
if ($HermesProfile -and $Tool -ne "hermes") {
Write-Warn "-Profile 仅对 -Tool hermes 生效,已忽略。"
$HermesProfile = ""
}
$selectedTools = @()
@@ -455,12 +514,22 @@ Write-Host " 仓库: $RepoRoot"
Write-Host " 安装到: $($selectedTools -join ', ')"
Write-Host ""
# 单个工具失败(如 Hermes 多 profile 未指定 -Profile)不应中断其余工具的安装(issue #102)
$script:FailedTools = @()
foreach ($t in $selectedTools) {
Install-Tool $t
}
$failedCount = @($script:FailedTools).Count
$installedCount = $selectedTools.Count - $failedCount
Write-Host ""
Write-OK "完成!已安装 $($selectedTools.Count) 个工具。"
if ($failedCount -gt 0) {
Write-Warn "完成:已安装 $installedCount 个工具,$failedCount 个未完成(见上方提示)。"
} else {
Write-OK "完成!已安装 $installedCount 个工具。"
}
Write-Host ""
Write-Dim "运行 .\scripts\convert.ps1 重新生成集成文件。"
Write-Host ""
if ($failedCount -gt 0) { exit 1 }
+83 -18
View File
@@ -6,7 +6,7 @@
# 请先运行 scripts/convert.sh 生成集成文件。
#
# 用法:
# ./scripts/install.sh [--tool <name>] [--no-interactive] [--help]
# ./scripts/install.sh [--tool <name>] [--profile <name>] [--no-interactive] [--help]
#
# 支持的工具:
# claude-code -- 复制到 ~/.claude/agents/
@@ -37,6 +37,14 @@
# Discord 模式下 Hermes 会把每个 skill 注册为斜杠命令,
# 总 JSON 超过 8000 字符会被 Discord API 拒绝 (error 50035)
# 若需要在 Discord 中使用建议按分类分批安装。
# --profile <名称> 多 profile 环境下指定安装到哪个 profileissue #102)。
# Hermes 每个 profile 有独立的 skill 库,路径为
# <base>/profiles/<名称>/skills/
# 其中 <base> = HERMES_HOME > $LOCALAPPDATA/hermes > ~/.hermes。
# 未指定 --profile 却检测到已存在多 profile 时,脚本会报错
# 退出而非静默装到根目录,避免装错位置。例如:
# --tool hermes --profile work
# --tool hermes --profile personal --category marketing
set -euo pipefail
@@ -63,7 +71,7 @@ ALL_TOOLS=(claude-code copilot antigravity gemini-cli opencode openclaw cursor t
# --- 用法 ---
usage() {
sed -n '3,26p' "$0" | sed 's/^# \{0,1\}//'
sed -n '3,47p' "$0" | sed 's/^# \{0,1\}//'
exit 0
}
@@ -91,7 +99,33 @@ detect_codex() { command -v codex >/dev/null 2>&1 || [[ -d "${HOME}/.code
detect_deerflow() { command -v deerflow >/dev/null 2>&1 || [[ -d "${HOME}/.deerflow" ]] || docker ps --format '{{.Names}}' 2>/dev/null | grep -q deerflow; }
detect_workbuddy() { command -v workbuddy >/dev/null 2>&1 || [[ -d "${HOME}/.workbuddy" ]]; }
detect_codewhale() { command -v codewhale >/dev/null 2>&1 || [[ -d "${HOME}/.codewhale" ]] || [[ -d "${HOME}/.deepseek" ]]; }
detect_hermes() { command -v hermes >/dev/null 2>&1 || [[ -d "${HOME}/.hermes" ]]; }
# Hermes 安装根目录(不含 profile):HERMES_HOME > Windows($LOCALAPPDATA/hermes) > ~/.hermesissue #82/#102
hermes_base_dir() {
if [[ -n "${HERMES_HOME:-}" ]]; then
printf '%s' "${HERMES_HOME}"
elif [[ -n "${LOCALAPPDATA:-}" && -d "${LOCALAPPDATA}/hermes" ]]; then
printf '%s' "${LOCALAPPDATA}/hermes"
else
printf '%s' "${HOME}/.hermes"
fi
}
# 根目录下是否存在多 profile<base>/profiles/<name>/
hermes_has_profiles() {
local base="$1" d
[[ -d "$base/profiles" ]] || return 1
for d in "$base/profiles"/*/; do [[ -d "$d" ]] && return 0; done
return 1
}
# 打印现有 profile 名称,逗号分隔(与 install.ps1 保持一致)
hermes_profile_names() {
local base="$1" d out=""
for d in "$base/profiles"/*/; do
[[ -d "$d" ]] || continue
out+="${out:+, }$(basename "$d")"
done
printf '%s' "$out"
}
detect_hermes() { command -v hermes >/dev/null 2>&1 || [[ -d "${HOME}/.hermes" ]] || [[ -n "${HERMES_HOME:-}" && -d "${HERMES_HOME}" ]] || { [[ -n "${LOCALAPPDATA:-}" && -d "${LOCALAPPDATA}/hermes" ]]; }; }
detect_kiro() { command -v kiro >/dev/null 2>&1 || command -v kiro-cli >/dev/null 2>&1 || [[ -d "${HOME}/.kiro" ]]; }
detect_qoder() { command -v qoder >/dev/null 2>&1 || [[ -d "${HOME}/.qoder" ]]; }
@@ -427,19 +461,34 @@ install_codewhale() {
install_hermes() {
local src="$INTEGRATIONS/hermes"
# 安装目录优先级(issue #82):官方环境变量 HERMES_HOME > Windows 新版默认位置 > 传统位置
local dest
if [[ -n "${HERMES_HOME:-}" ]]; then
dest="${HERMES_HOME}/skills"
elif [[ -n "${LOCALAPPDATA:-}" && -d "${LOCALAPPDATA}/hermes" ]]; then
dest="${LOCALAPPDATA}/hermes/skills"
else
dest="${HOME}/.hermes/skills"
fi
local count=0
[[ -d "$src" ]] || { err "integrations/hermes 不存在。请先运行 convert.sh --tool hermes"; return 1; }
# 安装目录解析(issue #82 / #102):
# <base> = HERMES_HOME > Windows($LOCALAPPDATA/hermes) > ~/.hermes
# 1. --profile <name> -> <base>/profiles/<name>/skills
# 2. 存在 profile 目录但未指定 --profile -> 报错退出(避免静默装到根目录)
# 3. 默认 -> <base>/skills
local base; base="$(hermes_base_dir)"
local dest profile_note=""
if [[ -n "${HERMES_PROFILE:-}" ]]; then
dest="${base}/profiles/${HERMES_PROFILE}/skills"
profile_note=" [profile: ${HERMES_PROFILE}]"
if [[ ! -d "${base}/profiles/${HERMES_PROFILE}" ]]; then
warn "profile '${HERMES_PROFILE}' 尚不存在,将新建目录 ${base}/profiles/${HERMES_PROFILE}/。"
warn "请确认名称无误(现有 profile: $(hermes_profile_names "$base"))。"
fi
elif hermes_has_profiles "$base"; then
err "检测到 Hermes profile 目录(${base}/profiles/),但未指定 --profile。"
err "为避免装错位置,请用 --profile <名称> 指定目标 profile。"
err "现有 profile: $(hermes_profile_names "$base")"
err "或设置 HERMES_HOME 指向单一 profile 根目录后重试。"
return 1
else
dest="${base}/skills"
fi
local count=0
# 若指定了 --category,只安装命中的分类;否则安装全部
local filter_note=""
if [[ ${#HERMES_CATEGORIES[@]} -gt 0 ]]; then
@@ -471,7 +520,7 @@ install_hermes() {
done < <(find "$catdir" -mindepth 1 -maxdepth 1 -type d -print0)
done < <(find "$src" -mindepth 1 -maxdepth 1 -type d -print0)
ok "Hermes Agent: $count 个 skills -> $dest$filter_note"
ok "Hermes Agent: $count 个 skills -> $dest$filter_note$profile_note"
if [[ ${#HERMES_CATEGORIES[@]} -eq 0 && $count -gt 80 ]]; then
warn "Hermes Discord 模式对斜杠命令总长有 8000 字符上限(error 50035)。"
warn "若要在 Discord 中使用,建议用 --category <名称> 按分类分批安装。"
@@ -546,11 +595,13 @@ install_tool() {
main() {
local tool="all"
HERMES_CATEGORIES=()
HERMES_PROFILE=""
while [[ $# -gt 0 ]]; do
case "$1" in
--tool) tool="${2:?'--tool 需要一个值'}"; shift 2 ;;
--category) HERMES_CATEGORIES+=("${2:?'--category 需要一个值'}"); shift 2 ;;
--profile|-p) HERMES_PROFILE="${2:?'--profile 需要一个值'}"; shift 2 ;;
--no-interactive) shift ;;
--help|-h) usage ;;
*) err "未知选项: $1"; usage ;;
@@ -561,6 +612,10 @@ main() {
warn "--category 仅对 --tool hermes 生效,已忽略。"
HERMES_CATEGORIES=()
fi
if [[ -n "$HERMES_PROFILE" && "$tool" != "hermes" ]]; then
warn "--profile 仅对 --tool hermes 生效,已忽略。"
HERMES_PROFILE=""
fi
check_integrations
@@ -605,17 +660,27 @@ main() {
printf " 安装到: %s\n" "${SELECTED_TOOLS[*]}"
printf "\n"
local installed=0 t
local installed=0 failed=0 t
for t in "${SELECTED_TOOLS[@]}"; do
install_tool "$t"
(( installed++ )) || true
# 单个工具失败(如 Hermes 多 profile 未指定 --profile)不应中断其余工具的安装
if install_tool "$t"; then
(( installed++ )) || true
else
(( failed++ )) || true
warn "$(tool_label "$t") 安装未完成(见上方提示)。"
fi
done
printf "\n"
ok "完成!已安装 $installed 个工具。"
if [[ $failed -gt 0 ]]; then
warn "完成:已安装 $installed 个工具,$failed 个未完成。"
else
ok "完成!已安装 $installed 个工具。"
fi
printf "\n"
dim " 运行 ./scripts/convert.sh 重新生成集成文件。"
printf "\n"
[[ $failed -eq 0 ]]
}
main "$@"