Files
wuguanganandClaude Opus 5 bae4f9af86 feat: 零配置装 hook;改名 Vibe Foreman Free
## 零配置

装法从三步变两步:拖进应用程序 → 打开。中间那步「双击安装 hook.command」
没有任何用户能做的决定,漏点它的后果却是**静默的** —— app 开着、
界面正常、一条事件都收不到,看起来像"装了但没用"。

HookInstaller 在启动时自己装。四条边界,都有测试钉着:

- 只碰自己的条目:只删命令里含 `hubctl hook` / `hub-hook-` 的(我们的旧版),
  用户手写的 hook 和 permissions / env 等其它键一律原样保留
- 幂等:内容没变一个字节都不写。`.sortedKeys` 是这条的前提 ——
  不排序时 JSON 键序跟着哈希走,"没变就不写"的判断会随机失效
- 坏 JSON 不覆盖:用户可能只是手改时漏了个逗号,覆盖等于删了他的配置
- 失败不阻断启动:hook 没装好的 app 仍能显示会话,起不来的 app 什么都不是

hubctl 每次比内容再决定要不要拷 —— 升级 app 后 hook 还指着旧二进制的话,
协议一变整条链路静默失效。设置页显示状态 + 一键修复。

dmg 里那个 command 文件改名成「手工修复 hook(通常用不到)」,留作兜底。

实测:本机 settings.json 装前装后逐键对比,顶层键无增无减、
permissions 等非 hooks 值全部原样、9 类 hook 各 1 条无重复;
第二次启动 sha256 未变。

## 改名

显示名 Claude Hub → Vibe Foreman Free。

**bundle id、签名证书名、可执行文件名 ClaudeHub 全部不动** ——
换任何一个都会让已授予的 TCC 权限(控制 iTerm 的 Automation、通知)失效,
而 `pgrep -x ClaudeHub` 那条判活约束也依赖可执行名(见 NOTES.md)。
安装时清掉旧名字那份,避免 /Applications 里同时躺着两个共用 bundle id 的 app。

530 个测试全过。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 22:27:28 +08:00

189 lines
7.4 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# 打发布包:universal .app + .dmg。
#
# 和 build-swift-app.sh 的分工:
# build-swift-app.sh 日常开发。只编当前架构,装进 /Applications,几秒钟。
# release.sh 发布。universal 双架构 + dmg + 校验和,几分钟。
#
# 刻意**不**装进 /Applications:发布构建和你正在用的那个是两回事,
# 打个包不该把你手上跑着的实例换掉。要用新版自己 open dmg 装。
set -euo pipefail
HUB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PKG_DIR="${HUB_DIR}/HubKit"
APP_NAME="Vibe Foreman Free"
BUNDLE_ID="dev.hengjun.claude-hub"
CERT_NAME="Claude Hub Dev"
VERSION="${1:-}"
if [ -z "${VERSION}" ]; then
VERSION=$(grep -m1 'CFBundleShortVersionString' "${HUB_DIR}/scripts/build-swift-app.sh" \
| sed -E 's/.*<string>([^<]+)<\/string>.*/\1/')
fi
# 构建号用提交数,比手工维护可靠:它单调递增,且能反查到具体代码。
BUILD_NUMBER=$(git -C "${HUB_DIR}" rev-list --count HEAD 2>/dev/null || echo 1)
COMMIT=$(git -C "${HUB_DIR}" rev-parse --short HEAD 2>/dev/null || echo "unknown")
DIRTY=""
if ! git -C "${HUB_DIR}" diff --quiet HEAD 2>/dev/null; then DIRTY="+dirty"; fi
STAGE="${PKG_DIR}/.build/release-stage"
APP_PATH="${STAGE}/${APP_NAME}.app"
OUT_DIR="${HUB_DIR}/release"
DMG_PATH="${OUT_DIR}/${APP_NAME// /-}-${VERSION}.dmg"
log() { printf '\033[36m▸\033[0m %s\n' "$1"; }
ok() { printf '\033[32m✓\033[0m %s\n' "$1"; }
warn() { printf '\033[33m!\033[0m %s\n' "$1"; }
fail() { printf '\033[31m✗\033[0m %s\n' "$1" >&2; exit 1; }
# ---------- 环境 ----------
command -v swift >/dev/null || fail "找不到 swift,请先安装 Xcode"
SDK_VERSION=$(xcrun --show-sdk-version)
[ "${SDK_VERSION%%.*}" -ge 26 ] || fail "需要 macOS 26 SDK(当前 ${SDK_VERSION})"
log "版本 ${VERSION} (build ${BUILD_NUMBER}, ${COMMIT}${DIRTY})"
if [ -n "${DIRTY}" ]; then
warn "工作区有未提交的改动 —— 这个包对应不到任何一个提交"
fi
# ---------- 测试 ----------
#
# 发布前必须全绿。日常构建可以跳过测试,发布不行。
log "跑测试…"
cd "${PKG_DIR}"
if ! swift test 2>&1 | tail -3; then
fail "测试没过,不打包"
fi
# ---------- 编译(双架构)----------
#
# `--arch arm64 --arch x86_64` 让 SwiftPM 直接产出 fat binary。
# macOS 26 是最后一个支持 Intel 的版本,还有人在上面跑,所以带上 x86_64。
log "编译 universal(arm64 + x86_64)…"
swift build -c release --arch arm64 --arch x86_64 --product ClaudeHub
swift build -c release --arch arm64 --arch x86_64 --product hubctl
BUILD_DIR="${PKG_DIR}/.build/apple/Products/Release"
[ -f "${BUILD_DIR}/ClaudeHub" ] || fail "没找到构建产物 ${BUILD_DIR}/ClaudeHub"
for binary in ClaudeHub hubctl; do
archs=$(lipo -archs "${BUILD_DIR}/${binary}")
case "$archs" in
*arm64*x86_64*|*x86_64*arm64*) ok "${binary}: ${archs}" ;;
*) fail "${binary} 不是 universal(${archs})" ;;
esac
done
# ---------- 组装 ----------
log "组装 ${APP_NAME}.app…"
rm -rf "${STAGE}"
mkdir -p "${APP_PATH}/Contents/MacOS" \
"${APP_PATH}/Contents/Resources" \
"${APP_PATH}/Contents/Helpers"
cp "${BUILD_DIR}/ClaudeHub" "${APP_PATH}/Contents/MacOS/ClaudeHub"
# hubctl 放 Helpers/ 而不是 MacOS/ —— 见 HubKit/NOTES.md,
# 和主程序共用 bundle identifier 会被 exec 瞬间 SIGKILL。
cp "${BUILD_DIR}/hubctl" "${APP_PATH}/Contents/Helpers/hubctl"
[ -f "${HUB_DIR}/Resources/AppIcon.icns" ] \
&& cp "${HUB_DIR}/Resources/AppIcon.icns" "${APP_PATH}/Contents/Resources/AppIcon.icns"
cat > "${APP_PATH}/Contents/Info.plist" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key> <string>${APP_NAME}</string>
<key>CFBundleDisplayName</key> <string>${APP_NAME}</string>
<key>CFBundleExecutable</key> <string>ClaudeHub</string>
<key>CFBundleIdentifier</key> <string>${BUNDLE_ID}</string>
<key>CFBundlePackageType</key> <string>APPL</string>
<key>CFBundleShortVersionString</key><string>${VERSION}</string>
<key>CFBundleVersion</key> <string>${BUILD_NUMBER}</string>
<key>CFBundleIconFile</key> <string>AppIcon</string>
<key>LSMinimumSystemVersion</key> <string>26.0</string>
<key>LSUIElement</key> <true/>
<key>NSAppleEventsUsageDescription</key>
<string>Vibe Foreman Free 需要控制终端来跳转到对应的会话标签页。</string>
<!-- 打包时的提交号。用户报问题时能直接对上代码。 -->
<key>HubGitCommit</key> <string>${COMMIT}${DIRTY}</string>
</dict>
</plist>
PLIST
# ---------- 签名 ----------
#
# 先签内层再签外层,不用 --deep(理由见 NOTES.md)。
if security find-certificate -c "${CERT_NAME}" >/dev/null 2>&1; then
SIGN_IDENTITY="${CERT_NAME}"
log "用「${CERT_NAME}」签名…"
else
SIGN_IDENTITY="-"
warn "没有自签名证书「${CERT_NAME}」,退回 ad-hoc 签名"
fi
codesign --force --sign "${SIGN_IDENTITY}" --identifier "${BUNDLE_ID}.hubctl" \
"${APP_PATH}/Contents/Helpers/hubctl"
codesign --force --sign "${SIGN_IDENTITY}" --identifier "${BUNDLE_ID}" "${APP_PATH}"
codesign --verify --deep --strict "${APP_PATH}" || fail "签名校验失败"
ok "签名通过"
# 冒烟测试:签名不对的话 hubctl 会被瞬间 SIGKILL,而那种失败没有任何输出。
"${APP_PATH}/Contents/Helpers/hubctl" doctor >/dev/null 2>&1 || true
if [ "$?" -gt 1 ]; then fail "hubctl 无法执行 —— 多半是签名问题"; fi
ok "hubctl 冒烟测试通过"
# ---------- dmg ----------
log "打 dmg…"
mkdir -p "${OUT_DIR}"
rm -f "${DMG_PATH}"
DMG_STAGE="${STAGE}/dmg"
rm -rf "${DMG_STAGE}"
mkdir -p "${DMG_STAGE}"
cp -R "${APP_PATH}" "${DMG_STAGE}/"
# 拖进去就装:给个 /Applications 的替身。
ln -s /Applications "${DMG_STAGE}/Applications"
# hook 现在由 app 首次启动时自己装(HookInstaller),dmg 里不再放那个
# 「安装 hook.command」—— 它没有任何用户能做的决定,而漏点它的后果是静默的:
# app 开着、界面正常、一条事件都收不到。
#
# 脚本本身留在仓库里,作为「装坏了怎么手工修」的兜底。
cp "${HUB_DIR}/scripts/setup-swift-hooks.sh" "${DMG_STAGE}/手工修复 hook(通常用不到).command" 2>/dev/null || true
chmod +x "${DMG_STAGE}/手工修复 hook(通常用不到).command" 2>/dev/null || true
hdiutil create -volname "${APP_NAME} ${VERSION}" \
-srcfolder "${DMG_STAGE}" -ov -format UDZO "${DMG_PATH}" >/dev/null
rm -rf "${DMG_STAGE}"
SIZE=$(du -h "${DMG_PATH}" | cut -f1)
SHA=$(shasum -a 256 "${DMG_PATH}" | cut -d' ' -f1)
echo
ok "完成:${DMG_PATH}"
echo " 版本 ${VERSION} (build ${BUILD_NUMBER}, ${COMMIT}${DIRTY})"
echo " 架构 arm64 + x86_64"
echo " 大小 ${SIZE}"
echo " SHA256 ${SHA}"
echo
cat <<'NOTICE'
分发提醒:这个包是**自签名、未公证**的。
你自己的机器上没问题(证书在你钥匙串里),但别人下载后打开会被 Gatekeeper 拦,
提示"无法打开,因为无法验证开发者"。对方需要:
右键点 app → 打开 → 再点"打开"
或者在终端里:
xattr -dr com.apple.quarantine "/Applications/Claude Hub.app"
要做到双击即用,需要 Apple Developer Program($99/年)的
Developer ID 证书 + notarytool 公证。
NOTICE