tmuxclihook 0.5.13 → 0.5.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmuxclihook",
3
- "version": "0.5.13",
3
+ "version": "0.5.15",
4
4
  "description": "交互式 TUI 安装器:环境侦测、AI CLI 侦测、tmux-ai-status hooks 装/卸/修、TPM 软链校验",
5
5
  "type": "module",
6
6
  "bin": {
@@ -219,7 +219,8 @@ adapter_uninstall_hooks() {
219
219
  local stripped
220
220
  stripped=$(_codex_read_existing | _codex_strip_managed)
221
221
  if [ "$(printf '%s' "$stripped" | jq -c .)" = "{}" ]; then
222
- rm -f "$ADAPTER_HOOKS_FILE"
222
+ # 符号链接(cc-switch 实例共享真源)只删真实目标,保留链接
223
+ _install_remove_target "$ADAPTER_HOOKS_FILE"
223
224
  echo "Codex hooks uninstalled (removed empty $ADAPTER_HOOKS_FILE)"
224
225
  else
225
226
  _install_atomic_write "$stripped" "$ADAPTER_HOOKS_FILE"
@@ -225,7 +225,8 @@ adapter_install_hooks() {
225
225
  adapter_uninstall_hooks() {
226
226
  if [ -f "$ADAPTER_HOOKS_FILE" ]; then
227
227
  if grep -q "$_OPENCODE_MARKER" "$ADAPTER_HOOKS_FILE" 2>/dev/null; then
228
- rm -f "$ADAPTER_HOOKS_FILE"
228
+ # 符号链接只删真实目标,保留链接(与原子写一致)
229
+ _install_remove_target "$ADAPTER_HOOKS_FILE"
229
230
  echo "opencode plugin uninstalled (removed $ADAPTER_HOOKS_FILE)"
230
231
  else
231
232
  echo "opencode plugin: $ADAPTER_HOOKS_FILE 非本插件生成,跳过删除"
@@ -45,6 +45,9 @@ esac
45
45
  case "${1:-install}" in
46
46
  uninstall)
47
47
  adapter_uninstall_hooks
48
+ # 先清安装意图再写卸载意图:suppress 写失败时 errexit 会退出,此顺序
49
+ # 保证 installed 不残留(残留会让 _refresh 把刚卸载的 hooks 装回来)。
50
+ _hooks_installed_clear claude || true
48
51
  _hooks_suppress_write claude
49
52
  ;;
50
53
  check)
@@ -66,15 +69,18 @@ case "${1:-install}" in
66
69
  # install 收敛。结构漂移(async/matcher 被外部改)不在判定内,auto 跳过
67
70
  # 不修复,显式 install 仍强收敛。
68
71
  if adapter_check_converged; then
72
+ _hooks_installed_ensure claude || true
69
73
  echo "claude hooks already converged (skip install)"
70
74
  exit 0
71
75
  fi
72
76
  adapter_install_hooks
73
77
  _hooks_suppress_clear claude || true
78
+ _hooks_installed_ensure claude || true
74
79
  ;;
75
80
  install)
76
81
  adapter_install_hooks
77
82
  _hooks_suppress_clear claude || true
83
+ _hooks_installed_ensure claude || true
78
84
  ;;
79
85
  *)
80
86
  echo "Usage: $0 [install|uninstall|check [--json]|auto]" >&2
@@ -40,6 +40,8 @@ esac
40
40
  case "${1:-install}" in
41
41
  uninstall)
42
42
  adapter_uninstall_hooks
43
+ # 先清安装意图再写卸载意图(理由同 claude wrapper)
44
+ _hooks_installed_clear codex || true
43
45
  _hooks_suppress_write codex
44
46
  ;;
45
47
  check)
@@ -55,6 +57,7 @@ case "${1:-install}" in
55
57
  install)
56
58
  adapter_install_hooks
57
59
  _hooks_suppress_clear codex || true
60
+ _hooks_installed_ensure codex || true
58
61
  ;;
59
62
  *)
60
63
  echo "Usage: $0 [install|uninstall|check [--json]]" >&2
@@ -40,6 +40,8 @@ esac
40
40
  case "${1:-install}" in
41
41
  uninstall)
42
42
  adapter_uninstall_hooks
43
+ # 先清安装意图再写卸载意图(理由同 claude wrapper)
44
+ _hooks_installed_clear opencode || true
43
45
  _hooks_suppress_write opencode
44
46
  ;;
45
47
  check)
@@ -55,6 +57,7 @@ case "${1:-install}" in
55
57
  install)
56
58
  adapter_install_hooks
57
59
  _hooks_suppress_clear opencode || true
60
+ _hooks_installed_ensure opencode || true
58
61
  ;;
59
62
  *)
60
63
  echo "Usage: $0 [install|uninstall|check [--json]]" >&2
@@ -15,14 +15,45 @@ _install_require_jq() {
15
15
  fi
16
16
  }
17
17
 
18
- # 原子写入:mktemp 与目标同目录 → 同文件系统 → mv 原子。
18
+ # 解析符号链接到最终真实路径(不要求目标存在——悬空链接同样要解析)。
19
+ # 相对链接基于链接所在目录解析;循环/超深链(>40 层)返回 1,调用方回退原路径。
20
+ # 用途:cc-switch 为每个 codex provider 建独立 CODEX_HOME,并把 hooks.json 做成
21
+ # 指向 ~/.codex/hooks.json 的符号链接(所有实例共享同一真源)。mv 作用于链接路径
22
+ # 时只替换链接本身,因此写入必须解析到真实目标,否则实例与共享真源分叉。
23
+ _install_real_target() { # $1=路径 → stdout 最终路径
24
+ local p="$1" link n=0
25
+ while [ -L "$p" ]; do
26
+ n=$((n + 1))
27
+ [ "$n" -le 40 ] || return 1
28
+ link=$(readlink "$p" 2>/dev/null) || return 1
29
+ [ -n "$link" ] || return 1
30
+ case "$link" in
31
+ /*) p="$link" ;;
32
+ *) p="$(dirname "$p")/$link" ;;
33
+ esac
34
+ done
35
+ printf '%s\n' "$p"
36
+ }
37
+
38
+ # 原子写入:mktemp 与真实目标同目录 → 同文件系统 → mv 原子。
19
39
  # 避免与 AI CLI 并发写产生竞态导致 hooks 字段丢失。
40
+ # 符号链接先解析:临时文件建在真实目标目录,mv 写穿链接(保留链接本身)。
20
41
  _install_atomic_write() {
21
- local content="$1" target="$2" tmp
22
- mkdir -p "$(dirname "$target")" 2>/dev/null || true
23
- tmp=$(mktemp "$(dirname "$target")/.hooks.XXXXXX")
42
+ local content="$1" target="$2" tmp real
43
+ real=$(_install_real_target "$target") || real="$target"
44
+ mkdir -p "$(dirname "$real")" 2>/dev/null || true
45
+ tmp=$(mktemp "$(dirname "$real")/.hooks.XXXXXX")
24
46
  printf '%s\n' "$content" > "$tmp"
25
- mv -f "$tmp" "$target"
47
+ mv -f "$tmp" "$real"
48
+ }
49
+
50
+ # 删除写入目标(uninstall 清空时用):符号链接解析后只删真实目标,保留链接
51
+ # (指向悬空,下次 install 写穿即恢复共享)。直接 rm 链接会删掉链接本身,把
52
+ # 共享真源留在原地——其他实例仍被注册,语义与「卸载」矛盾。
53
+ _install_remove_target() { # $1=路径
54
+ local target="$1" real
55
+ real=$(_install_real_target "$target") || real="$target"
56
+ rm -f "$real"
26
57
  }
27
58
 
28
59
  # --- 卸载意图标记(suppression marker)---
@@ -55,6 +86,32 @@ _hooks_suppress_clear() {
55
86
  fi
56
87
  }
57
88
 
89
+ # --- 安装意图标记(installed marker)---
90
+ # 显式 install / claude auto(含已收敛跳过)→ 写;显式 uninstall → 删。
91
+ # 语义 = 用户装过该工具;跨工具自修复(_maybe_repair_other_tools)据此决定
92
+ # hooks 丢失时是否允许自动重装。无标记 + check 失败时绝不安装——尊重
93
+ # 「codex/opencode 非人人安装」的现状。
94
+ # ⚠️ 与 suppressed 同属 _SUPPRESS_DIR 两处同步约束(另一处见 lib-tmux-ai-status.sh)。
95
+ _hooks_installed_file() { printf '%s/installed-%s' "$_SUPPRESS_DIR" "$1"; }
96
+ _hooks_installed_write() {
97
+ if ! mkdir -p "$_SUPPRESS_DIR" 2>/dev/null \
98
+ || ! date +%s > "$(_hooks_installed_file "$1")" 2>/dev/null; then
99
+ echo "WARN: 安装意图标记写入失败——该工具 hooks 丢失后不会自动修复;" \
100
+ "请检查 $_SUPPRESS_DIR 可写性" >&2
101
+ return 1
102
+ fi
103
+ }
104
+ _hooks_installed_clear() {
105
+ if [ -e "$(_hooks_installed_file "$1")" ] \
106
+ && ! rm -f "$(_hooks_installed_file "$1")" 2>/dev/null; then
107
+ echo "WARN: 安装意图标记删除失败(卸载仍由 suppressed 标记兜底)" >&2
108
+ return 1
109
+ fi
110
+ }
111
+ _hooks_installed_ensure() { # 已登记则不动,避免 mtime 抖动
112
+ [ -e "$(_hooks_installed_file "$1")" ] || _hooks_installed_write "$1"
113
+ }
114
+
58
115
  # --- 安装/卸载读-改-写互斥锁 ---
59
116
  # 写路径的并发方:TUI 显式装 / tmux 入口 auto / SessionStart 自修复 / _maybe_repair_hooks /
60
117
  # drift 收敛——五方都经 wrapper dispatch,锁在此层一处覆盖三 adapter。
@@ -10,6 +10,17 @@ _STATUS_DIR="${TMUX_AI_STATUS_DIR:-/tmp/ai-status}"
10
10
  _SUPPRESS_DIR="${TMUX_AI_SUPPRESS_DIR:-${XDG_CONFIG_HOME:-${HOME}/.config}/tmuxclihook}"
11
11
  STATUS_COLOR="#F1FA8C"
12
12
 
13
+ # 安装意图标记路径(与 lib-install-hooks.sh 的 _hooks_installed_file 同构)。
14
+ # 事件路径不 source 安装库,故此处独立实现;目录定义同 suppressed 两处同步。
15
+ # 语义:用户显式装过该工具 → 跨工具自修复允许在其 hooks 丢失时自动重装。
16
+ _installed_marker_path() { printf '%s/installed-%s' "$_SUPPRESS_DIR" "$1"; }
17
+ _installed_marker_write() {
18
+ local f
19
+ f=$(_installed_marker_path "$1") || return 1
20
+ mkdir -p "$_SUPPRESS_DIR" 2>/dev/null || return 1
21
+ date +%s > "$f" 2>/dev/null
22
+ }
23
+
13
24
  # --- 日志模块 ---
14
25
  source "${_LIB_DIR}/lib-tmux-ai-log.sh"
15
26
 
@@ -907,7 +918,15 @@ _cleanup_stale_panes() {
907
918
  local ps_snap
908
919
  ps_snap=$(_ps_process_snapshot)
909
920
 
910
- while IFS='|' read -r pane_id pane_status; do
921
+ # 动态追加 monitor seen 字段(与 build_all_status 同一组名字,复用其收集器)。
922
+ # launcher/daemon 场景工具进程可能脱离 pane 进程树,进程树 BFS 看不到;
923
+ # seen=1 是 monitor 用 launcher 进程树探测得出的「工具确实存活」信号。
924
+ local seen_fmt="" _seen_name
925
+ for _seen_name in $(_collect_ai_process_names); do
926
+ seen_fmt="${seen_fmt}|#{@ai_monitor_seen_${_seen_name}}"
927
+ done
928
+
929
+ while IFS='|' read -r pane_id pane_status _seen_flags; do
911
930
  case "$pane_status" in
912
931
  "!"|"?"|">")
913
932
  local sanitized="${pane_id//[^a-zA-Z0-9]/_}"
@@ -923,13 +942,20 @@ _cleanup_stale_panes() {
923
942
  fi
924
943
  ;;
925
944
  "✓"|"-")
926
- if ! _pane_has_ai_process "$pane_id" "$ps_snap"; then
927
- _ai_log "STALE: clearing terminal state '$pane_status' on $pane_id (no ai process)"
928
- tmux set-option -pt "$pane_id" @ai_pane_status "" 2>/dev/null || true
929
- fi
945
+ # 终态只认「进程存活」证据:seen=1 时保留(launcher/daemon 场景
946
+ # 进程树不可见);活跃态 !/?/> 的判据是归属证据,不使用 seen。
947
+ case "$_seen_flags" in
948
+ *1*) ;;
949
+ *)
950
+ if ! _pane_has_ai_process "$pane_id" "$ps_snap"; then
951
+ _ai_log "STALE: clearing terminal state '$pane_status' on $pane_id (no ai process)"
952
+ tmux set-option -pt "$pane_id" @ai_pane_status "" 2>/dev/null || true
953
+ fi
954
+ ;;
955
+ esac
930
956
  ;;
931
957
  esac
932
- done < <(tmux list-panes -a -F "#{pane_id}|#{@ai_pane_status}" 2>/dev/null)
958
+ done < <(tmux list-panes -a -F "#{pane_id}|#{@ai_pane_status}${seen_fmt}" 2>/dev/null)
933
959
 
934
960
  # 清理孤儿目录:目录存在但对应 pane 已不在 tmux 中
935
961
  local all_pane_ids
@@ -1039,3 +1065,47 @@ _maybe_repair_hooks() {
1039
1065
  fi
1040
1066
  fi
1041
1067
  }
1068
+
1069
+ # 跨工具自修复:tmux 入口所有 _refresh/_session-switched 固定传 TOOL_ID=claude,
1070
+ # codex/opencode 只有自身 SessionStart 能触发修复;hooks 文件被整体覆盖/删除后
1071
+ # 再无事件 → 永不自愈。本函数由 _run_refresh 调用,遍历其余 adapter,对
1072
+ # 「用户显式装过」(installed-<tool> 标记)的工具做 check → 缺失则 install。
1073
+ #
1074
+ # 不 source adapter:ADAPTER_* 与 adapter_* 会覆盖当前已加载的 adapter(TOOL_ID
1075
+ # 污染),一律子进程 exec wrapper(wrapper 自带 _LIB_DIR 与 adapter 加载)。
1076
+ # 未登记的工具只做「既存安装登记」:check 通过才补写标记,绝不自动安装——
1077
+ # 尊重 codex/opencode 非人人安装的现状。
1078
+ # 成本:每工具每 60s 至多 1 次 check;installed 存在且缺失时再加 1 次 install。
1079
+ _maybe_repair_other_tools() {
1080
+ local current="${TOOL_ID:-claude}"
1081
+ local f tool installer marker now last
1082
+ for f in "${_ADAPTER_DIR}"/*.sh; do
1083
+ [ -f "$f" ] || continue
1084
+ tool=$(basename "$f" .sh)
1085
+ [ "$tool" = "$current" ] && continue
1086
+ [ -f "${_SUPPRESS_DIR}/suppressed-${tool}" ] && continue
1087
+ installer="${_LIB_DIR}/install-${tool}-hooks.sh"
1088
+ [ -x "$installer" ] || continue
1089
+ marker="${_STATUS_DIR}/.hooks-repair-${tool}-ts"
1090
+ now=$(date +%s)
1091
+ [ -d "$_STATUS_DIR" ] || mkdir -p "$_STATUS_DIR" 2>/dev/null
1092
+ if [ -f "$marker" ]; then
1093
+ last=$(cat "$marker" 2>/dev/null)
1094
+ [ $((now - ${last:-0})) -lt 60 ] && continue
1095
+ fi
1096
+ echo "$now" > "$marker" 2>/dev/null || true
1097
+ if [ -f "$(_installed_marker_path "$tool")" ]; then
1098
+ "$installer" check >/dev/null 2>&1 && continue
1099
+ _ai_log "REPAIR: ${tool} hooks missing, reinstalling (cross-tool)"
1100
+ if "$installer" >/dev/null 2>&1; then
1101
+ tmux display-message "✓ ${tool} hooks 已自动修复(检测到缺失)" 2>/dev/null || true
1102
+ else
1103
+ tmux display-message "⚠ ${tool} hooks 自动修复失败,请运行 tmuxclihook 检查" 2>/dev/null || true
1104
+ fi
1105
+ elif "$installer" check >/dev/null 2>&1; then
1106
+ # 未登记但健康:登记既存安装(老用户升级迁移),绝不安装
1107
+ _installed_marker_write "$tool" 2>/dev/null || true
1108
+ _ai_log "REPAIR: ${tool} hooks healthy, registered installed marker"
1109
+ fi
1110
+ done
1111
+ }
@@ -108,6 +108,9 @@ _notification_implicit_status() {
108
108
  # 重建聚合状态(_refresh / _entered 共用)
109
109
  _run_refresh() {
110
110
  _maybe_repair_hooks
111
+ # 跨工具修复:_refresh 固定以 claude 身份运行,codex/opencode 的整体丢失
112
+ # 只能靠这里兜底(仅对 installed-<tool> 已登记的工具)。
113
+ _maybe_repair_other_tools
111
114
  # 节流版清理(60s):tmux 生命周期 hook(pane-focus-in 等)高频触发,
112
115
  # 全价 _cleanup_stale_panes 实测 ~455ms;卡死状态清理有 :37 的事件路径
113
116
  # 节流兜底,此处无需每次全价。Stop/SessionEnd fallback 仍走全价精准清理。
@@ -85,7 +85,8 @@ tmux bind-key r run-shell "'${CURRENT_DIR}/scripts/tmux-ai-reload'" 2>/dev/null
85
85
  # #{hook_client_name} 这个 format 变量——误用它恒展开为空,handler 的
86
86
  # [ -n "$3" ] 守卫静默跳过,entered_time 从不写入,排序退化为 created 序。
87
87
  tmux set-hook -g client-session-changed "run-shell '${CURRENT_DIR}/scripts/tmux-ai-status claude _session-switched #{hook_client}'" 2>/dev/null || true
88
- # 注:_refresh 与 TOOL_ID 无关(仅重建聚合 + 自修复 claude hooks),统一传 claude。
88
+ # 注:_refresh 与 TOOL_ID 无关(重建聚合 + 自修复当前工具 hooks + 跨工具修复
89
+ # 已显式安装的 codex/opencode),统一传 claude。
89
90
  tmux set-hook -g session-closed "run-shell '${CURRENT_DIR}/scripts/tmux-ai-status claude _refresh'"
90
91
  tmux set-hook -g client-detached "run-shell '${CURRENT_DIR}/scripts/tmux-ai-status claude _refresh'"
91
92
  # (client-attached 不再注册:attach 场景由 client-session-changed 覆盖)