Files
vibe-foreman/scripts/setup.sh
T
b6d90cac2b feat: 重写为原生 Swift 灵动岛,修掉通知跳转与主窗口数据链路 (#4)
删掉 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>
2026-07-29 19:20:12 +08:00

128 lines
4.4 KiB
Bash
Executable File
Raw 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
# claude-hub 初始化脚本
# 在新电脑上运行一次即可:bash scripts/setup.sh
#
# 1. 注入全局 hooks(Stop + Notification)
# 2. 确保脚本有执行权限
# 3. 验证配置
CYAN='\033[0;36m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
BOLD='\033[1m'
NC='\033[0m'
HUB_DIR="$(cd "$(dirname "$0")/.." && pwd)"
SETTINGS_FILE="$HOME/.claude/settings.json"
echo ""
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${BOLD} Claude 调度中心 - 初始化${NC}"
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""
# 确保 ~/.claude 目录和 settings.json 存在
mkdir -p "$HOME/.claude"
if [ ! -f "$SETTINGS_FILE" ]; then
echo '{}' > "$SETTINGS_FILE"
echo -e "${YELLOW}创建了 $SETTINGS_FILE${NC}"
fi
# 注入 hooks
#
# 这里**不再自己写 settings.json**,一律转交 setup-swift-hooks.sh。
#
# 原因:这个脚本以前会注入 hub-hook-stop.sh / hub-hook-permission.sh 那套 bash hook。
# 它们和现在的 hubctl 是互斥的两套实现,而且这个脚本跑在后面就会把 hubctl 的配置
# 覆盖回去 —— 「装完 hook 又不生效」的经典来源。配置只允许有一个出口。
if ! bash "$HUB_DIR/scripts/setup-swift-hooks.sh"; then
echo -e "${RED}✗ hook 配置失败${NC}"
exit 1
fi
# 初始化 projects.yaml
if [ ! -f "$HUB_DIR/projects.yaml" ]; then
if [ -f "$HUB_DIR/projects.yaml.example" ]; then
cp "$HUB_DIR/projects.yaml.example" "$HUB_DIR/projects.yaml"
echo -e "${YELLOW}已从 example 创建 projects.yaml,请编辑添加你的项目${NC}"
echo -e "${YELLOW} 或运行: bash scripts/scan-projects.sh ~/Documents/code${NC}"
fi
else
echo -e "${GREEN}✓ projects.yaml 已存在${NC}"
fi
# 配置 iTerm2 tmux 集成(命令行直接写 plist,无需手动去 GUI 勾选)
if command -v defaults &>/dev/null; then
# tmux 窗口显示为 iTerm2 原生标签页(0=Native windows, 2=Tabs in new window)
defaults write com.googlecode.iterm2 OpenTmuxWindowsIn -int 2
# 连接后自动隐藏 tmux 控制会话
defaults write com.googlecode.iterm2 AutoHideTmuxClientSession -bool true
echo -e "${GREEN}✓ iTerm2 tmux 集成已配置${NC}"
fi
# 确保脚本有执行权限
chmod +x "$HUB_DIR/scripts/"*.sh
echo -e "${GREEN}✓ 脚本权限已设置${NC}"
# 创建信号目录
mkdir -p /tmp/hub-signals
echo -e "${GREEN}✓ 信号目录已创建${NC}"
# 生成语音文件
SOUNDS_DIR="$HUB_DIR/sounds"
mkdir -p "$SOUNDS_DIR"
if [ ! -f "$SOUNDS_DIR/done1.aiff" ]; then
say -o "$SOUNDS_DIR/done1.aiff" "搞定了,来看看"
say -o "$SOUNDS_DIR/done2.aiff" "干完了,等你验收"
say -o "$SOUNDS_DIR/done3.aiff" "这边好了"
say -o "$SOUNDS_DIR/done4.aiff" "完事儿了"
say -o "$SOUNDS_DIR/done5.aiff" "做好了,请过目"
say -o "$SOUNDS_DIR/auth1.aiff" "需要你授权"
say -o "$SOUNDS_DIR/auth2.aiff" "在等你点确认"
say -o "$SOUNDS_DIR/auth3.aiff" "卡住了,需要你批准"
echo -e "${GREEN}✓ 语音文件已生成${NC}"
else
echo -e "${GREEN}✓ 语音文件已存在${NC}"
fi
# 验证
echo ""
echo -e "${BOLD}验证:${NC}"
for cmd in tmux say osascript terminal-notifier; do
if command -v $cmd &>/dev/null; then
echo -e "${GREEN}✓ $cmd 可用${NC}"
else
echo -e "${RED}✗ $cmd 不可用${NC}"
fi
done
for script in hub-run.sh hub-notify.sh project-menu.sh welcome.sh; do
if [ -f "$HUB_DIR/scripts/$script" ]; then
echo -e "${GREEN}✓ $script${NC}"
else
echo -e "${YELLOW}⚠ $script 不存在${NC}"
fi
done
echo ""
echo -e "${GREEN}${BOLD}初始化完成!${NC}"
echo ""
echo -e "全局 hook 已配置:"
echo -e " ${CYAN}Stop${NC} → 灵动岛闯入提示 + 可点击跳转的通知"
echo -e " ${CYAN}Notification${NC} → Claude 需要输入/授权时提示"
echo -e " ${CYAN}SessionEnd${NC} → 会话结束时清理岛上的条目"
echo -e " ${CYAN}PreToolUse${NC} → 高风险操作在岛上审批"
echo ""
echo -e "${YELLOW}子窗口的 Claude 需要重启才能生效。${NC}"
# 编译并安装 app
if [ ! -d "/Applications/Claude Hub.app" ]; then
echo ""
echo -e "${YELLOW}Claude Hub.app 尚未安装,正在编译(首次需要几分钟)...${NC}"
bash "$HUB_DIR/scripts/build-swift-app.sh"
else
echo -e "${GREEN}✓ Claude Hub.app 已安装${NC}"
fi