tmuxclihook 0.2.2 → 0.3.0

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/README.md CHANGED
@@ -32,8 +32,8 @@ hooks 安装、tmux 集成写入的路径由「source」决定:环境变量 `T
32
32
 
33
33
  | 菜单项 | 说明 |
34
34
  |--------|------|
35
- | 环境检查 | 侦测 tmux(≥3.1) / jq / bash(任意版本) / node(≥18),显示 `✓ ready`、`✗ missing`、`⚠ outdated`,并在确认后安全修复 |
36
- | 侦测 AI CLI | 扫描 Claude Code / Codex / opencode,显示版本、最低版本和 hooks 状态;不自动安装或升级 AI CLI |
35
+ | 环境检查 | 侦测 tmux(≥3.1) / jq / bash(任意版本) / node(≥18),显示 `✓ ready`、`✗ missing`、`⚠ outdated`,并在确认后安全修复。检测到 cc-switch(`~/.cc-switch/cc-switch.db`)时追加 sqlite3 项 |
36
+ | 侦测 AI CLI | 扫描 Claude Code / Codex / opencode,显示版本、最低版本和 hooks 状态;不自动安装或升级 AI CLI。cc-switch 环境下 Claude Code 的 hooks 状态由 bash 侧 `install-claude-hooks.sh check`(sqlite 真源)判定 |
37
37
  | 安装 hooks | 选工具(claude/codex/opencode/全部)→ 调 `scripts/install-<tool>-hooks.sh`(路径随当前 source 解析) |
38
38
  | 卸载 hooks | 对称卸载本插件 hooks,保留其他工具注册的 hook |
39
39
  | 修复 hooks | 完整性检查 → 缺失则重装 |
@@ -41,6 +41,10 @@ hooks 安装、tmux 集成写入的路径由「source」决定:环境变量 `T
41
41
  | source 管理 | 查看当前生效 source(来源 + 失效警告);设置本地仓库路径用于开发调试;清除持久化配置恢复默认 |
42
42
  | 完整卸载插件 | 停止 Codex monitor、清除聚合状态 `@ai_all_status`、清理历史遗留软链(不删仓库);`.tmux.conf` 删声明与 `tmux kill-server` 因破坏性仅打印手动指引 |
43
43
 
44
+ ### cc-switch 模式(Claude Code)
45
+
46
+ 检测到 `~/.cc-switch/cc-switch.db` 时,Claude Code hooks 的装/卸/修/状态检查全部指向其通用配置(db `settings` 表 `common_config_claude`)——`switch` 命令启动 claude 时会把通用配置合并进实例配置,`~/.claude/settings.json` 与实例文件都会被 cc-switch 覆盖,直接写它们不持久。写入用与 cc-switch 自身一致的 `json_patch` 原子更新,他人 hook 与配置其他字段保留。注意:cc-switch GUI 运行中会把内存缓存写回 db,安装前建议先退出 GUI(脚本会警告);不经 `switch` 直接运行的 `claude` 读不到 db 里的 hooks。
47
+
44
48
  ## Source 管理(生产默认 / 本地调试切换)
45
49
 
46
50
  `npm i -g` 装完后,hooks 安装与 tmux 集成默认使用包内自带的 `scripts/`(由 `npm publish` 前的 `prepack` 步骤从仓库根复制打包进来,见「架构」一节),开箱即用,无需仓库 checkout。
package/bin/cli.js CHANGED
@@ -6,7 +6,7 @@
6
6
  //
7
7
  // "source" 决定 scripts/ 与 tmux 入口来自哪里:环境变量 TMUXCLIHOOK_SOURCE >
8
8
  // 持久化配置 ~/.config/tmuxclihook/config.json > 包内自带(生产默认)> 仓库根
9
- // (开发回落)。见 src/source.js 与 docs/2026-07-15-npm-global-install-plan.md。
9
+ // (开发回落)。见 src/source.js。
10
10
 
11
11
  import { homedir } from 'node:os';
12
12
  import * as p from '@clack/prompts';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmuxclihook",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "交互式 TUI 安装器:环境侦测、AI CLI 侦测、tmux-ai-status hooks 装/卸/修、TPM 软链校验",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,7 +25,8 @@ ADAPTER_EVENTS="SessionStart SessionEnd UserPromptSubmit PreToolUse PostToolUse
25
25
  # 前台进程名(BFS pane 进程树匹配 basename)
26
26
  ADAPTER_PROCESS_NAMES="claude"
27
27
 
28
- # hooks 配置:~/.claude/settings.json(CLAUDE_CONFIG_DIR 可覆盖)
28
+ # hooks 配置:~/.claude/settings.json(CLAUDE_CONFIG_DIR 可覆盖)。
29
+ # 仅在非 cc-switch 环境作为写入目标;cc-switch 环境见 _ccswitch_active。
29
30
  ADAPTER_HOOKS_FILE="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json"
30
31
 
31
32
  # Claude Code 有 SessionEnd 事件,退出即发 hook → 状态可主动清。
@@ -38,34 +39,25 @@ ADAPTER_HOLD_UNMATCHED_PERMISSION="false"
38
39
  # 自修复/TUI 调用的安装脚本
39
40
  ADAPTER_INSTALLER="${_LIB_DIR}/install-claude-hooks.sh"
40
41
 
41
- # 完整性检查:settings.json 的 10 个事件是否都注册了本插件 hook。
42
- # 缺失(外部应用覆盖配置)→ 返回 1。
43
- adapter_check_integrity() {
44
- [ -f "$ADAPTER_HOOKS_FILE" ] || return 1
45
- local registered
46
- registered=$(jq '
47
- .hooks as $h |
48
- ["SessionStart","SessionEnd","UserPromptSubmit","PreToolUse","PostToolUse",
49
- "PostToolUseFailure","PermissionRequest","Notification","Stop","StopFailure"]
50
- | map(select([$h[.][]?.hooks[]?.command // empty] | any(contains("tmux-ai-status"))))
51
- | length
52
- ' "$ADAPTER_HOOKS_FILE" 2>/dev/null)
53
- [ "${registered:-0}" -ge 10 ]
54
- }
55
-
56
42
  # Claude 事件集合(含同步事件 PermissionRequest)。install/uninstall 用。
57
43
  _CLAUDE_EVENTS="SessionStart SessionEnd UserPromptSubmit PreToolUse PostToolUse PostToolUseFailure PermissionRequest Notification Stop StopFailure"
58
44
 
59
- # 安装:合并式更新 settings.json,保留他人(masko 等)注册的 hook。
60
- # 清理 legacy 名(tmux-claude-status / tmux-powerline-claude-status)+ 指向其他路径的
61
- # stale tmux-ai-status 条目,$TOOL_CMD 未注册则追加。PermissionRequest 为 sync(async=false)。
62
- adapter_install_hooks() {
63
- _install_require_jq
64
- [ -f "$ADAPTER_HOOKS_FILE" ] || _install_atomic_write '{}' "$ADAPTER_HOOKS_FILE"
65
- local hook_script="${_LIB_DIR}/tmux-ai-status"
66
- local tool_cmd="${hook_script} claude"
67
- local updated
68
- updated=$(jq --arg dev_script "$hook_script" --arg tool_cmd "$tool_cmd" '
45
+ # ─── jq 程序常量:文件模式与 cc-switch 模式共用同一合并/剥离/计数核心 ───
46
+ # 程序文本内无单引号,可安全放入单引号 shell 变量。
47
+
48
+ # 完整性计数:10 个事件中注册了含 tmux-ai-status 的 command 的事件数。
49
+ _CLAUDE_JQ_COUNT='
50
+ .hooks as $h |
51
+ ["SessionStart","SessionEnd","UserPromptSubmit","PreToolUse","PostToolUse",
52
+ "PostToolUseFailure","PermissionRequest","Notification","Stop","StopFailure"]
53
+ | map(select([$h[.][]?.hooks[]?.command // empty] | any(contains("tmux-ai-status"))))
54
+ | length
55
+ '
56
+
57
+ # 合并式安装:剥 legacy 名(tmux-claude-status / tmux-powerline-claude-status)+
58
+ # 指向其他路径的 stale tmux-ai-status 条目,$tool_cmd 未注册则追加。
59
+ # PermissionRequest 为 sync(async=false)。
60
+ _CLAUDE_JQ_MERGE='
69
61
  ["SessionStart","SessionEnd","UserPromptSubmit","PreToolUse","PostToolUse",
70
62
  "PostToolUseFailure","PermissionRequest","Notification","Stop","StopFailure"] as $events |
71
63
  ["PermissionRequest"] as $sync_events |
@@ -99,19 +91,10 @@ adapter_install_hooks() {
99
91
  }]
100
92
  else . end
101
93
  )
102
- ' "$ADAPTER_HOOKS_FILE")
103
- _install_atomic_write "$updated" "$ADAPTER_HOOKS_FILE"
104
- echo "Claude hooks installed to $ADAPTER_HOOKS_FILE"
105
- echo "Events: $_CLAUDE_EVENTS"
106
- }
94
+ '
107
95
 
108
- # 卸载:单次 jq pipeline 移除所有指向本插件的 hooks,保留他人条目。
109
- adapter_uninstall_hooks() {
110
- _install_require_jq
111
- [ -f "$ADAPTER_HOOKS_FILE" ] || { echo "Claude hooks: nothing to uninstall"; return 0; }
112
- local hook_script="${_LIB_DIR}/tmux-ai-status"
113
- local updated
114
- updated=$(jq --arg hook_script "$hook_script" '
96
+ # 剥离:移除所有指向本插件的条目,保留他人 hook,空 group/事件删除。
97
+ _CLAUDE_JQ_STRIP='
115
98
  .hooks |= if . then
116
99
  [. | to_entries[] |
117
100
  .value |= [
@@ -121,7 +104,164 @@ adapter_uninstall_hooks() {
121
104
  | select(.value | length > 0)
122
105
  ] | from_entries
123
106
  else . end
124
- ' "$ADAPTER_HOOKS_FILE")
107
+ '
108
+
109
+ _claude_apply_merge() { # $1=输入 JSON 文件 → stdout 变换后全文档
110
+ jq --arg dev_script "${_LIB_DIR}/tmux-ai-status" \
111
+ --arg tool_cmd "${_LIB_DIR}/tmux-ai-status claude" \
112
+ "$_CLAUDE_JQ_MERGE" "$1"
113
+ }
114
+
115
+ _claude_apply_strip() { # $1=输入 JSON 文件 → stdout 变换后全文档
116
+ jq --arg hook_script "${_LIB_DIR}/tmux-ai-status" "$_CLAUDE_JQ_STRIP" "$1"
117
+ }
118
+
119
+ _claude_count_registered() { # $1=输入 JSON 文件 → stdout 事件数(jq 失败时为空)
120
+ jq "$_CLAUDE_JQ_COUNT" "$1" 2>/dev/null
121
+ }
122
+
123
+ # ─── cc-switch 通用配置支持 ───
124
+ # `switch` 命令启动 claude 时把 ~/.cc-switch/cc-switch.db 的通用配置(settings 表
125
+ # common_config_claude,JSON)深合并进实例 settings.json(provider meta 均
126
+ # commonConfigEnabled=true)。~/.claude/settings.json 与实例文件都会被 cc-switch
127
+ # 覆盖,通用配置是 hooks 的唯一持久真源。检测到 cc-switch 时装/卸/完整性只操作
128
+ # 该库;sqlite3 缺失等一切门禁不过 → 整体回退传统文件模式。
129
+
130
+ _ccswitch_db() { printf '%s\n' "${HOME}/.cc-switch/cc-switch.db"; }
131
+
132
+ # 门禁:db 存在 && sqlite3 可用 && settings 表可查。不要求 key 存在
133
+ # (从未编辑过通用配置的用户由 install 的 upsert 建行;若此处要求 key,
134
+ # 会静默回退文件模式 → 实例永不含 hooks + 自修复空转)。
135
+ _ccswitch_active() {
136
+ local db
137
+ db="$(_ccswitch_db)"
138
+ [ -f "$db" ] || return 1
139
+ command -v sqlite3 >/dev/null 2>&1 || return 1
140
+ sqlite3 -readonly "$db" \
141
+ "SELECT 1 FROM sqlite_master WHERE type='table' AND name='settings'" 2>/dev/null \
142
+ | grep -q 1
143
+ }
144
+
145
+ # cc-switch GUI 运行中会把内存缓存的通用配置全量写回 db(实测发生过),警告不阻塞。
146
+ _ccswitch_warn_gui() {
147
+ if pgrep -ix "cc-switch" >/dev/null 2>&1 \
148
+ || pgrep -f "cc-switch.app/Contents/MacOS" >/dev/null 2>&1; then
149
+ echo "WARN: cc-switch GUI 正在运行,其内存缓存可能在退出时覆盖本次写入;" \
150
+ "建议退出 GUI 后重跑安装确认。tmux 会话活跃时自修复会在 60s 内重写。" >&2
151
+ fi
152
+ }
153
+
154
+ # 读通用配置 value → mktemp 文件,路径写入 $1 命名的变量。
155
+ # 行缺失/空白 → "{}";非空但非法 JSON → 明确报错 return 1(绝不覆盖用户数据)。
156
+ # 注意:内部临时变量名不得与调用方常用名(tmp 等)同名——bash 动态作用域下
157
+ # printf -v "$1" 会写到本函数的同名 local,函数返回即丢失。
158
+ _ccswitch_dump_common() { # $1=out var name
159
+ local _cc_out_file
160
+ _cc_out_file=$(mktemp "${TMPDIR:-/tmp}/ccswitch-common.XXXXXX") || return 1
161
+ sqlite3 -readonly "$(_ccswitch_db)" \
162
+ "SELECT value FROM settings WHERE key='common_config_claude'" > "$_cc_out_file" 2>/dev/null
163
+ if [ ! -s "$_cc_out_file" ] || [ -z "$(tr -d '[:space:]' < "$_cc_out_file")" ]; then
164
+ printf '{}\n' > "$_cc_out_file"
165
+ elif ! jq -e . "$_cc_out_file" >/dev/null 2>&1; then
166
+ rm -f "$_cc_out_file"
167
+ echo "ERROR: cc-switch 通用配置不是合法 JSON,拒绝操作。排查:" >&2
168
+ echo " sqlite3 -readonly $(_ccswitch_db) \"SELECT value FROM settings WHERE key='common_config_claude'\"" >&2
169
+ return 1
170
+ fi
171
+ printf -v "$1" '%s' "$_cc_out_file"
172
+ }
173
+
174
+ # SQL 字符串字面量转义(' → ''),与 cc-launch 写回通用配置的 sqlQuote 同规则。
175
+ _sqlquote() { printf "'%s'" "$(printf '%s' "$1" | sed "s/'/''/g")"; }
176
+
177
+ # 单条 upsert 写库:行存在 → json_patch 深合并(RFC 7396,只动 patch 内的键);
178
+ # 行缺失 → 整文档插入。与 cc-launch 写回通用配置同款机制。
179
+ _ccswitch_upsert_hooks() { # $1=hooks patch JSON(紧凑) $2=完整文档 JSON(行缺失时插入用)
180
+ local sql
181
+ sql="INSERT INTO settings(key, value) VALUES('common_config_claude', json($(_sqlquote "$2")))
182
+ ON CONFLICT(key) DO UPDATE SET value = json_patch(settings.value, json($(_sqlquote "$1")))"
183
+ if ! sqlite3 -cmd ".timeout 5000" "$(_ccswitch_db)" "$sql" 2>/dev/null; then
184
+ echo "ERROR: 写入 cc-switch.db 失败(库可能被占用;tmux 会话活跃时自修复会重试)" >&2
185
+ return 1
186
+ fi
187
+ }
188
+
189
+ # 完整性检查:cc-switch 模式查通用配置;否则查 hooks 配置文件。
190
+ # 10 个事件都注册了本插件 hook 才算完整;缺失(外部覆盖)→ 返回 1 触发自修复。
191
+ adapter_check_integrity() {
192
+ local registered tmp
193
+ if _ccswitch_active; then
194
+ _ccswitch_dump_common tmp || return 1
195
+ registered=$(_claude_count_registered "$tmp")
196
+ rm -f "$tmp"
197
+ else
198
+ [ -f "$ADAPTER_HOOKS_FILE" ] || return 1
199
+ registered=$(_claude_count_registered "$ADAPTER_HOOKS_FILE")
200
+ fi
201
+ [ "${registered:-0}" -ge 10 ]
202
+ }
203
+
204
+ # 安装:合并式更新,保留他人(masko 等)注册的 hook。cc-switch 模式只写库
205
+ # (~/.claude/settings.json 与实例文件都会被 cc-switch 覆盖,写了也不持久)。
206
+ adapter_install_hooks() {
207
+ _install_require_jq
208
+ if _ccswitch_active; then
209
+ local tmp merged hooks_patch full_doc
210
+ _ccswitch_warn_gui
211
+ _ccswitch_dump_common tmp || return 1
212
+ merged=$(_claude_apply_merge "$tmp")
213
+ rm -f "$tmp"
214
+ hooks_patch=$(jq -c '{hooks: (.hooks // {})}' <<<"$merged")
215
+ full_doc=$(jq -c . <<<"$merged")
216
+ _ccswitch_upsert_hooks "$hooks_patch" "$full_doc" || return 1
217
+ echo "Claude hooks installed to cc-switch common config: $(_ccswitch_db)"
218
+ echo "Events: $_CLAUDE_EVENTS"
219
+ return 0
220
+ fi
221
+ [ -f "$ADAPTER_HOOKS_FILE" ] || _install_atomic_write '{}' "$ADAPTER_HOOKS_FILE"
222
+ local updated
223
+ updated=$(_claude_apply_merge "$ADAPTER_HOOKS_FILE")
224
+ _install_atomic_write "$updated" "$ADAPTER_HOOKS_FILE"
225
+ echo "Claude hooks installed to $ADAPTER_HOOKS_FILE"
226
+ echo "Events: $_CLAUDE_EVENTS"
227
+ }
228
+
229
+ # 卸载:单次 jq pipeline 移除所有指向本插件的 hooks,保留他人条目。
230
+ # cc-switch 模式:剥离前后对照生成 patch(被清空的事件键置 null = RFC 7396 删键),
231
+ # 顶层其他字段(model/env/…)由 json_patch 天然保留。
232
+ adapter_uninstall_hooks() {
233
+ _install_require_jq
234
+ if _ccswitch_active; then
235
+ local tmp stripped cur_hooks final_hooks patch managed top_patch
236
+ _ccswitch_dump_common tmp || return 1
237
+ cur_hooks=$(jq -c '.hooks // {}' "$tmp")
238
+ # 已无本插件条目 → no-op(否则会把保留的他人 hook 重写一遍)
239
+ managed=$(jq '[to_entries[] | select(.value | any(.hooks[]?.command | contains("tmux-ai-status")))] | length' <<<"$cur_hooks")
240
+ if [ "${managed:-0}" -eq 0 ]; then
241
+ rm -f "$tmp"
242
+ echo "Claude hooks: nothing to uninstall (cc-switch common config)"
243
+ return 0
244
+ fi
245
+ stripped=$(_claude_apply_strip "$tmp")
246
+ final_hooks=$(jq -c '.hooks // {}' <<<"$stripped")
247
+ rm -f "$tmp"
248
+ patch=$(jq -cn --argjson cur "$cur_hooks" --argjson fin "$final_hooks" '
249
+ reduce ($cur | keys[]) as $k ({};
250
+ if ($fin | has($k)) then .[$k] = $fin[$k] else .[$k] = null end)')
251
+ if [ "$patch" = "{}" ]; then
252
+ echo "Claude hooks: nothing to uninstall (cc-switch common config)"
253
+ return 0
254
+ fi
255
+ # patch 是事件级键,需包一层 {hooks: …} 才是顶层 patch(json_patch 只动 hooks)
256
+ local top_patch
257
+ top_patch=$(jq -cn --argjson p "$patch" '{hooks: $p}')
258
+ _ccswitch_upsert_hooks "$top_patch" "$top_patch" || return 1
259
+ echo "Claude hooks uninstalled from cc-switch common config"
260
+ return 0
261
+ fi
262
+ [ -f "$ADAPTER_HOOKS_FILE" ] || { echo "Claude hooks: nothing to uninstall"; return 0; }
263
+ local updated
264
+ updated=$(_claude_apply_strip "$ADAPTER_HOOKS_FILE")
125
265
  _install_atomic_write "$updated" "$ADAPTER_HOOKS_FILE"
126
266
  echo "Claude hooks uninstalled from $ADAPTER_HOOKS_FILE"
127
267
  }
@@ -1,9 +1,11 @@
1
1
  #!/bin/bash
2
- # install-claude-hooks.sh — Claude Code hooks 安装/卸载 wrapper
3
- # 用法: install-claude-hooks.sh [uninstall]
2
+ # install-claude-hooks.sh — Claude Code hooks 安装/卸载/检查 wrapper
3
+ # 用法: install-claude-hooks.sh [uninstall|check]
4
4
  #
5
5
  # 薄 wrapper:source 公共骨架 + claude adapter,dispatch 到 adapter_install_hooks /
6
6
  # adapter_uninstall_hooks。实际逻辑在 scripts/adapters/claude.sh。
7
+ # check:输出 ok/missing(exit 0/1),供 TUI 侦测在 cc-switch 模式下走 bash 侧
8
+ # 完整性判定(sqlite 真源),与非 cc-switch 的文件判定共用同一真相。
7
9
 
8
10
  set -o errexit
9
11
  set -o pipefail
@@ -14,6 +16,13 @@ source "${_LIB_DIR}/adapters/claude.sh"
14
16
 
15
17
  if [ "${1:-install}" = "uninstall" ]; then
16
18
  adapter_uninstall_hooks
19
+ elif [ "${1:-}" = "check" ]; then
20
+ if adapter_check_integrity; then
21
+ echo ok
22
+ else
23
+ echo missing
24
+ exit 1
25
+ fi
17
26
  else
18
27
  adapter_install_hooks
19
28
  fi
@@ -29,6 +29,10 @@ function buildTools(scriptsDir) {
29
29
  installer: join(scriptsDir, 'install-claude-hooks.sh'),
30
30
  hookMarker: 'tmux-ai-status',
31
31
  minHookCount: 10,
32
+ // cc-switch 存在时 hooks 真源在其 db(通用配置),文件判定失效 → 由 bash 侧
33
+ // wrapper check 判定(内部走 sqlite);非 cc-switch 环境仍走本地文件计数。
34
+ checkViaWrapper: true,
35
+ ccSwitchDb: join(HOME, '.cc-switch', 'cc-switch.db'),
32
36
  },
33
37
  {
34
38
  id: 'codex',
package/src/detect.js CHANGED
@@ -1,6 +1,8 @@
1
1
  // detect.js — 环境依赖侦测 + AI CLI 侦测
2
2
  import { execa } from 'execa';
3
3
  import { existsSync, readFileSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { homedir } from 'node:os';
4
6
  import { TOOLS, versionGte } from './adapters-meta.js';
5
7
 
6
8
  // 探测某命令是否存在 + 版本
@@ -80,6 +82,23 @@ export async function checkEnv({ probeCommand = probe } = {}) {
80
82
  pkg: 'node',
81
83
  }));
82
84
 
85
+ // sqlite3:仅 cc-switch 环境需要(hooks 真源在其 db 通用配置,装/卸/检查走 sqlite)
86
+ const ccSwitchDb = join(homedir(), '.cc-switch', 'cc-switch.db');
87
+ if (existsSync(ccSwitchDb)) {
88
+ const sqlite3 = await probeCommand('sqlite3', ['--version'], /(\d+\.\d+)/);
89
+ results.push(envResult({
90
+ name: 'sqlite3',
91
+ found: sqlite3.found,
92
+ meetsMinimum: sqlite3.found,
93
+ detail: sqlite3.found
94
+ ? `v${sqlite3.version}(cc-switch 模式)`
95
+ : '未安装(cc-switch 模式需要)',
96
+ min: '任意版本',
97
+ fixCmd: 'brew install sqlite3',
98
+ pkg: 'sqlite3',
99
+ }));
100
+ }
101
+
83
102
  return results;
84
103
  }
85
104
 
@@ -90,7 +109,7 @@ export function interactiveShell() {
90
109
  return { path: sh || '未知', name };
91
110
  }
92
111
 
93
- // 侦测某工具是否已装本插件 hooks
112
+ // 侦测某工具是否已装本插件 hooks(同步文件版:非 cc-switch 路径/向后兼容)
94
113
  function hooksInstalled(tool) {
95
114
  try {
96
115
  if (!existsSync(tool.hooksFile)) return false;
@@ -102,6 +121,23 @@ function hooksInstalled(tool) {
102
121
  }
103
122
  }
104
123
 
124
+ // 异步判定:声明 checkViaWrapper 且 cc-switch db 存在时,走 bash wrapper check
125
+ // (bash 侧按 sqlite 通用配置判定真源);否则回落本地文件计数。
126
+ async function hooksInstalledAsync(tool) {
127
+ if (tool.checkViaWrapper && tool.ccSwitchDb && existsSync(tool.ccSwitchDb)) {
128
+ try {
129
+ const { stdout, exitCode } = await execa('bash', [tool.installer, 'check'], {
130
+ reject: false,
131
+ timeout: 15000,
132
+ });
133
+ return exitCode === 0 && String(stdout).trim() === 'ok';
134
+ } catch {
135
+ return false;
136
+ }
137
+ }
138
+ return hooksInstalled(tool);
139
+ }
140
+
105
141
  // 侦测所有已装 AI CLI
106
142
  export async function detectClis() {
107
143
  const rows = [];
@@ -115,10 +151,10 @@ export async function detectClis() {
115
151
  version: p.version,
116
152
  meetsMin,
117
153
  minVersion: tool.minVersion,
118
- hooksInstalled: p.found ? hooksInstalled(tool) : false,
154
+ hooksInstalled: p.found ? await hooksInstalledAsync(tool) : false,
119
155
  });
120
156
  }
121
157
  return rows;
122
158
  }
123
159
 
124
- export { hooksInstalled };
160
+ export { hooksInstalled, hooksInstalledAsync };
package/src/hooks.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { execa } from 'execa';
3
3
  import { existsSync } from 'node:fs';
4
4
  import { toolById } from './adapters-meta.js';
5
- import { hooksInstalled } from './detect.js';
5
+ import { hooksInstalledAsync } from './detect.js';
6
6
 
7
7
  // 安装某工具 hooks(调薄 wrapper install-<tool>-hooks.sh)
8
8
  export async function installHooks(id) {
@@ -30,11 +30,12 @@ export async function uninstallHooks(id) {
30
30
  }
31
31
  }
32
32
 
33
- // 修复某工具 hooks:完整性检查(JS 侧 hooksInstalled)→ 缺失则重装
33
+ // 修复某工具 hooks:完整性检查(cc-switch 模式走 bash wrapper check,否则文件计数)
34
+ // → 缺失则重装
34
35
  export async function repairHooks(id) {
35
36
  const tool = toolById(id);
36
37
  if (!tool) return { ok: false, output: `未知工具: ${id}` };
37
- if (hooksInstalled(tool)) {
38
+ if (await hooksInstalledAsync(tool)) {
38
39
  return { ok: true, output: `${tool.label} hooks 完整,无需修复`, repaired: false };
39
40
  }
40
41
  const r = await installHooks(id);
package/src/tmuxconf.js CHANGED
@@ -3,8 +3,7 @@
3
3
  // 加载机制不再依赖 TPM 风格的 `@plugin` 声明与软链,而是直接一行
4
4
  // `run-shell '<当前 source 的 tmux 入口绝对路径>'`。TMUX_ENTRY 随 source.js
5
5
  // 的三级解析(env / 持久化配置 / 包内自带 / 开发回落)而变化,切换 source 后
6
- // 需要重新调用 ensureTmuxIntegration() 才会把 .tmux.conf 里的路径更新到位
7
- // (见 docs/2026-07-15-npm-global-install-plan.md 的残留风险第 4 条)。
6
+ // 需要重新调用 ensureTmuxIntegration() 才会把 .tmux.conf 里的路径更新到位。
8
7
  import { execa } from 'execa';
9
8
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
10
9
  import { join } from 'node:path';