删掉 Tauri 版(app/),全部重写为 SwiftUI + SwiftPM(HubKit/,8.2k 行源码 + 2k 行测试,131 个测试)。核心动机是旧版"点通知跳不到对应终端"—— 根因是身份链路全程用会漂移的字符串:项目名 → tmux 窗口名 → pane_title → AppleScript 子串匹配。实测 9 个 tmux 窗口只有 2 个能和 projects.yaml 对上, 其中一个窗口名是 `2.1.173`(tmux automatic-rename 把它改成了 claude 的版本号)。 主要变化 - 会话状态直接读 ~/.claude/sessions/<PID>.json(Claude Code 自己维护的权威源), 不再用 spinner 字符猜 busy;死进程的残留 json 用 kill(pid,0) 过滤 - 跳转改用 iTerm jobPid + 进程祖先链求交,全程只有整数 PID 和 UUID; bg 会话不在任何终端里,单独用稳定 job 名兜底。实测 9/9 绑定成功 - hook 从 bash 换成 Swift 的 hubctl,走 Unix socket;Hook 没运行时立即放行 - 高风险操作在岛上审批,六重防误点;拒绝是超时和异常的默认值 - 主窗口:会话 / 项目 / 用量 / 审批日志 / 设置 修掉的真实缺陷(详见 HubKit/NOTES.md) - 超时阶梯是反的:ApprovalCoordinator 和 hubctl 读超时都是 60s,同时到期时 hubctl 按 fail-open 放行了一条已被服务端判定拒绝的 git push --force origin main。 改成 60/65/75/90 严格递增,并区分「连不上」(放行)与「连上但没应答」(拒绝) - 点击全部被吞:app 是 .accessory + panel 是 .nonactivatingPanel, NSHostingView.acceptsFirstMouse 默认 false,每次点击都算 first mouse 被丢弃 - hitTest 假设 NSHostingView 恒为 flipped,而 isFlipped 是 final 的可读写属性 - 形态切换靠替换 rootView,导致动画不可能发生且 @State 每次归零 - 全局 leftMouseDown 监听会收到点在自己身上的点击,展开态一点就收起 - hook 进程死后审批卡不消失,且会挡住后面的请求(按 clientPid 扫描清理) - app 读的 projects.yaml 不是脚本维护的那份,且那份被写坏成 `projects: []` + 117 条内容,解析器静默返回 0 —— 三个页面因此全空 - 用量对 945MB/341 个 jsonl 整份读进内存,跑不完;改流式 + 增量缓存后 7 天首扫 3.6s、命中缓存 0.22s、全量 4s - 辅助二进制放 Contents/MacOS/ 且共用 bundle id 会被 exec 瞬间 SIGKILL - 同一 bundle id 对应多个 .app 让存活判断和 TCC 授权互相打架 刻意的取舍 - 放弃 Liquid Glass 做岛的基底:它会跟着背景亮度双向补偿,而刘海恒为纯黑, 两者物理上互斥。改用固定 86% 暗色基底 + 背景模糊 + 边缘高光 - 用 SwiftPM + 打包脚本而不是 .xcodeproj:手写 xcodeproj 的 XML 无法 review, 且 SwiftPM 保住了 swift test Co-authored-by: wuguangan <anjaihd0206@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
184 lines
7.0 KiB
Bash
Executable File
184 lines
7.0 KiB
Bash
Executable File
#!/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="Claude Hub"
|
||
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>Claude Hub 需要控制终端来跳转到对应的会话标签页。</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"
|
||
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
|