tmuxclihook 0.5.1 → 0.5.3
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 +11 -4
- package/bin/cli.js +2 -11
- package/package.json +1 -1
- package/scripts/adapters/claude.sh +164 -93
- package/src/adapters-meta.js +3 -2
- package/src/check-detail.js +2 -1
- package/src/detect.js +2 -1
- package/src/help.js +31 -0
- package/src/update.js +39 -12
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ tmuxclihook
|
|
|
12
12
|
# 按当前安装方式(npm/volta)检查并更新到最新版
|
|
13
13
|
tmuxclihook update
|
|
14
14
|
|
|
15
|
-
#
|
|
15
|
+
# 删除不生效的另一份安装;已是最新时也会执行清理(默认交互确认)
|
|
16
16
|
tmuxclihook update --prune
|
|
17
17
|
|
|
18
18
|
# hooks 健康明细(非交互):每个 CLI 的完整性、判定真源、注册路径、漂移警告
|
|
@@ -37,11 +37,11 @@ npm start
|
|
|
37
37
|
|
|
38
38
|
hooks 安装、tmux 集成写入的路径由「source」决定:环境变量 `TMUXCLIHOOK_SOURCE` > 持久化配置 `~/.config/tmuxclihook/config.json`(TUI「source 管理」菜单写入)> 包内自带 scripts/(生产默认)> 仓库根 scripts/(`cd installer && npm start` 时的开发回落)。见下方「source 管理」一节。
|
|
39
39
|
|
|
40
|
-
`tmuxclihook update` 会显示当前版本,通过 `npm view tmuxclihook version`
|
|
40
|
+
`tmuxclihook update` 会显示当前版本,通过 `npm view tmuxclihook version` 查询最新版;版本相同则不做任何安装(若显式传入 `--prune`,仍会执行一次非生效副本检测与清理)。需要升级时,它按 PATH 上第一个可执行的 `tmuxclihook` 判断当前生效的安装方式:入口位于 `$VOLTA_HOME/bin/`(默认 `~/.volta/bin/`)或 volta 的 packages 镜像内时,只执行 `volta install tmuxclihook@latest`;其余入口(npm 全局、nvm/fnm 等 node 工具链目录)只执行 `npm install -g tmuxclihook@latest`。不会同时写入两套安装。网络查询或安装失败时命令以非零状态退出。
|
|
41
41
|
|
|
42
42
|
安装完成后会按 PATH 自检实际生效版本。若生效入口不是 volta、自检仍未反映新版,且本机可执行 volta,则补执行一次 `volta install tmuxclihook@latest` 作为兜底并复检;仍不一致时打印警告(不影响退出码)。
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
升级成功,或版本已是最新且显式传入 `--prune` 时,update 会检查另一套安装(volta packages 镜像或当前 npm 全局 prefix)是否还留有副本。发现副本时:TTY 下询问是否删除(默认不删);非交互环境只打印警告;`--prune` 跳过询问直接删除。删除前会校验该副本未被任何 hooks 与 `~/.tmux.conf` 引用——任一检查无法确认就保留并提示先重装 hooks。**生效入口永远不删**,清理失败不影响升级结果。也可以手动收敛:卸载不再使用的那份(`npm uninstall -g tmuxclihook` 或 `volta uninstall tmuxclihook`),再从生效入口重新安装 hooks。
|
|
45
45
|
|
|
46
46
|
## 功能
|
|
47
47
|
|
|
@@ -79,7 +79,14 @@ clack 0.7 只把 ctrl+c 映射为取消(Esc 默认无效果),安装器在
|
|
|
79
79
|
|
|
80
80
|
### cc-switch 模式(Claude Code)
|
|
81
81
|
|
|
82
|
-
检测到 `~/.cc-switch/cc-switch.db` 时,Claude Code hooks
|
|
82
|
+
检测到 `~/.cc-switch/cc-switch.db` 时,Claude Code hooks 的装/卸/修/状态检查改走 cc-switch 的共享配置。真源位置随 cc-switch 版本自动切换(判定规则与 cc-switch CLI 的 `detectSharedConfigMode` 一致,`CC_SWITCH_SHARED_SOURCE=live|legacy` 可显式覆盖):
|
|
83
|
+
|
|
84
|
+
| cc-switch 版本 | 判定 | hooks 真源 / 写入目标 |
|
|
85
|
+
|---|---|---|
|
|
86
|
+
| 4.0.2+(live) | `PRAGMA user_version >= 20` | `~/.claude/settings.json` |
|
|
87
|
+
| 旧版(legacy) | `user_version < 20` 或读不到 | db `settings` 表 `common_config_claude` |
|
|
88
|
+
|
|
89
|
+
live 模式以 `~/.claude/settings.json` 为共享配置真源(cc-switch 4.0.2 起不再读库里的 `common_config_claude`),写入用与文件模式相同的原子合并,他人 hook 与配置其他字段保留;legacy 模式用与 cc-switch 自身一致的 `json_patch` 更新 db。注意:cc-switch GUI 运行中会把内存缓存写回真源,安装前建议先退出 GUI(脚本会警告);legacy 模式下不经 `switch` 直接运行的 `claude` 读不到 db 里的 hooks。
|
|
83
90
|
|
|
84
91
|
## Source 管理(生产默认 / 本地调试切换)
|
|
85
92
|
|
package/bin/cli.js
CHANGED
|
@@ -31,6 +31,7 @@ import { setSource, clearSource, getPersistedSource, originLabel } from '../src/
|
|
|
31
31
|
import { enableEscapeCancel, setSpinnerActive } from '../src/esc.js';
|
|
32
32
|
import { note, padDisplay } from '../src/tui.js';
|
|
33
33
|
import { updatePackage, CURRENT_VERSION } from '../src/update.js';
|
|
34
|
+
import { HELP_TEXT } from '../src/help.js';
|
|
34
35
|
|
|
35
36
|
const OK = '✓';
|
|
36
37
|
const NO = '✗';
|
|
@@ -505,17 +506,7 @@ if (arg === 'update') {
|
|
|
505
506
|
} else if (arg === '--version' || arg === '-v' || arg === 'version') {
|
|
506
507
|
console.log(`tmuxclihook v${CURRENT_VERSION}`);
|
|
507
508
|
} else if (arg === '--help' || arg === '-h' || arg === 'help') {
|
|
508
|
-
console.log(
|
|
509
|
-
'tmuxclihook — tmux-ai-status 交互式安装器\n' +
|
|
510
|
-
'\n' +
|
|
511
|
-
'用法:\n' +
|
|
512
|
-
' tmuxclihook 打开交互式 TUI 安装器\n' +
|
|
513
|
-
' tmuxclihook check hooks 健康明细(可跟 claude|codex|opencode;exit 1 = 有缺失或漂移)\n' +
|
|
514
|
-
' tmuxclihook update 按当前安装方式(npm/volta)检查并更新到最新版本\n' +
|
|
515
|
-
' tmuxclihook update --prune 升级后直接删除不生效的另一份安装(默认交互确认)\n' +
|
|
516
|
-
' tmuxclihook version 显示版本(--version / -v 等价)\n' +
|
|
517
|
-
' tmuxclihook help 显示本帮助(--help / -h 等价)',
|
|
518
|
-
);
|
|
509
|
+
console.log(HELP_TEXT);
|
|
519
510
|
} else {
|
|
520
511
|
main().catch((err) => {
|
|
521
512
|
console.error(err);
|
package/package.json
CHANGED
|
@@ -119,28 +119,54 @@ _claude_count_registered() { # $1=输入 JSON 文件 → stdout 事件数(jq
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
# ─── cc-switch 通用配置支持 ───
|
|
122
|
-
# `switch` 命令启动 claude
|
|
123
|
-
#
|
|
124
|
-
#
|
|
125
|
-
#
|
|
126
|
-
#
|
|
122
|
+
# `switch` 命令启动 claude 时把「共享配置」深合并进实例 settings.json
|
|
123
|
+
# (provider meta 均 commonConfigEnabled=true)。~/.claude/settings.json 与实例
|
|
124
|
+
# 文件都会被 cc-switch 覆盖,共享配置才是 hooks 的唯一持久真源。
|
|
125
|
+
#
|
|
126
|
+
# 共享配置的真源位置随 cc-switch 版本变化(规则与 cc-switch CLI 的
|
|
127
|
+
# detectSharedConfigMode 保持一致):
|
|
128
|
+
# - legacy(< 4.0.2 / db PRAGMA user_version < 20):
|
|
129
|
+
# 真源 = db settings 表的 common_config_claude(JSON)
|
|
130
|
+
# - live(4.0.2+ / user_version >= 20):
|
|
131
|
+
# 真源 = ~/.claude/settings.json —— 4.0.2 起 cc-switch 不再读库里的
|
|
132
|
+
# common_config_claude(仅作迁移快照),所以新机器上写库等于写进死信。
|
|
133
|
+
# - CC_SWITCH_SHARED_SOURCE=live|legacy 可显式覆盖(cc-switch 同款逃生阀)
|
|
134
|
+
# 门禁不过(db/表/sqlite3 缺失)→ none,整体回退传统文件模式。
|
|
127
135
|
|
|
128
136
|
_ccswitch_db() { printf '%s\n' "${HOME}/.cc-switch/cc-switch.db"; }
|
|
129
137
|
|
|
130
|
-
#
|
|
131
|
-
#
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
138
|
+
# live 模式真源:cc-switch 硬编码 $HOME/.claude/settings.json,
|
|
139
|
+
# 不随 CLAUDE_CONFIG_DIR 变化(后者指向的是 switch 实例目录)。
|
|
140
|
+
_ccswitch_live_file() { printf '%s\n' "${HOME}/.claude/settings.json"; }
|
|
141
|
+
|
|
142
|
+
# 输出 live|legacy|none;恒返回 0(调用方无需容忍失败分支)。
|
|
143
|
+
_ccswitch_mode() {
|
|
144
|
+
local db override version
|
|
135
145
|
db="$(_ccswitch_db)"
|
|
136
|
-
[ -f "$db" ] ||
|
|
137
|
-
|
|
138
|
-
|
|
146
|
+
if [ ! -f "$db" ] || ! command -v sqlite3 >/dev/null 2>&1; then
|
|
147
|
+
printf 'none\n'
|
|
148
|
+
return 0
|
|
149
|
+
fi
|
|
150
|
+
if ! sqlite3 -readonly "$db" \
|
|
139
151
|
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='settings'" 2>/dev/null \
|
|
140
|
-
| grep -q 1
|
|
152
|
+
| grep -q 1; then
|
|
153
|
+
printf 'none\n'
|
|
154
|
+
return 0
|
|
155
|
+
fi
|
|
156
|
+
override=$(printf '%s' "${CC_SWITCH_SHARED_SOURCE:-}" | tr '[:upper:]' '[:lower:]')
|
|
157
|
+
case "$override" in
|
|
158
|
+
live|legacy) printf '%s\n' "$override"; return 0 ;;
|
|
159
|
+
esac
|
|
160
|
+
# 读不到版本(空/非数字)按 legacy——与 cc-switch CLI 的 null → legacy 一致
|
|
161
|
+
version=$(sqlite3 -readonly "$db" "PRAGMA user_version" 2>/dev/null | tr -cd '0-9')
|
|
162
|
+
if [ -n "$version" ] && [ "$version" -ge 20 ]; then
|
|
163
|
+
printf 'live\n'
|
|
164
|
+
else
|
|
165
|
+
printf 'legacy\n'
|
|
166
|
+
fi
|
|
141
167
|
}
|
|
142
168
|
|
|
143
|
-
# cc-switch GUI
|
|
169
|
+
# cc-switch GUI 运行中会把内存缓存的共享配置全量写回真源(实测发生过),警告不阻塞。
|
|
144
170
|
_ccswitch_warn_gui() {
|
|
145
171
|
if pgrep -ix "cc-switch" >/dev/null 2>&1 \
|
|
146
172
|
|| pgrep -f "cc-switch.app/Contents/MacOS" >/dev/null 2>&1; then
|
|
@@ -190,13 +216,22 @@ ON CONFLICT(key) DO UPDATE SET value = json_patch(settings.value, json($(_sqlquo
|
|
|
190
216
|
# 存活提取内联在本函数(jq + test),不依赖 lib-install-hooks.sh——本函数同时被
|
|
191
217
|
# 事件路径(lib-tmux-ai-status.sh)与 install wrapper(lib-install-hooks.sh)两个上下文调用。
|
|
192
218
|
adapter_check_integrity() {
|
|
193
|
-
local src registered ev p ok_events=""
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
219
|
+
local mode src="" tmp="" registered ev p ok_events=""
|
|
220
|
+
mode=$(_ccswitch_mode)
|
|
221
|
+
case "$mode" in
|
|
222
|
+
live)
|
|
223
|
+
src=$(_ccswitch_live_file)
|
|
224
|
+
[ -f "$src" ] || return 1
|
|
225
|
+
;;
|
|
226
|
+
legacy)
|
|
227
|
+
_ccswitch_dump_common tmp || return 1
|
|
228
|
+
src="$tmp"
|
|
229
|
+
;;
|
|
230
|
+
*)
|
|
231
|
+
[ -f "$ADAPTER_HOOKS_FILE" ] || return 1
|
|
232
|
+
src="$ADAPTER_HOOKS_FILE"
|
|
233
|
+
;;
|
|
234
|
+
esac
|
|
200
235
|
registered=$(_claude_count_registered "$src")
|
|
201
236
|
if [ "${registered:-0}" -ge 10 ]; then
|
|
202
237
|
while IFS=$'\t' read -r ev p; do
|
|
@@ -212,9 +247,7 @@ adapter_check_integrity() {
|
|
|
212
247
|
select(($scripts | length) > 0) |
|
|
213
248
|
$scripts[] | "\($e)\t\(.)"' "$src" 2>/dev/null)
|
|
214
249
|
fi
|
|
215
|
-
|
|
216
|
-
rm -f "$src"
|
|
217
|
-
fi
|
|
250
|
+
[ -z "$tmp" ] || rm -f "$tmp"
|
|
218
251
|
[ "${registered:-0}" -ge 10 ] || return 1
|
|
219
252
|
for ev in SessionStart SessionEnd UserPromptSubmit PreToolUse PostToolUse PostToolUseFailure PermissionRequest Notification Stop StopFailure; do
|
|
220
253
|
case ",$ok_events," in *",$ev,"*) ;; *) return 1 ;; esac
|
|
@@ -229,20 +262,38 @@ adapter_check_integrity() {
|
|
|
229
262
|
# status 判定仍由 integrity 本体独立给出,两者永不发散。
|
|
230
263
|
# 任何失败(jq 失败/配置缺失)输出空 events 的降级片段,不 exit 非 0。
|
|
231
264
|
adapter_check_detail() {
|
|
232
|
-
local
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
265
|
+
local source_mode src="" tmp="" pairs rows line ev p a
|
|
266
|
+
source_mode=$(_ccswitch_mode)
|
|
267
|
+
case "$source_mode" in
|
|
268
|
+
live)
|
|
269
|
+
src=$(_ccswitch_live_file)
|
|
270
|
+
[ -f "$src" ] || src=""
|
|
271
|
+
;;
|
|
272
|
+
legacy)
|
|
273
|
+
if ! _ccswitch_dump_common tmp; then
|
|
274
|
+
printf '{"mode":"ccswitch","events":{},"plugin":null,"registeredPaths":[]}\n'
|
|
275
|
+
return 0
|
|
276
|
+
fi
|
|
277
|
+
src="$tmp"
|
|
278
|
+
;;
|
|
279
|
+
*)
|
|
280
|
+
if [ -f "$ADAPTER_HOOKS_FILE" ]; then src="$ADAPTER_HOOKS_FILE"; fi
|
|
281
|
+
;;
|
|
282
|
+
esac
|
|
242
283
|
if [ -z "$src" ]; then
|
|
243
|
-
|
|
284
|
+
case "$source_mode" in
|
|
285
|
+
live) source_mode="ccswitch-live" ;;
|
|
286
|
+
legacy) source_mode="ccswitch" ;;
|
|
287
|
+
*) source_mode="file" ;;
|
|
288
|
+
esac
|
|
289
|
+
printf '{"mode":"%s","events":{},"plugin":null,"registeredPaths":[]}\n' "$source_mode"
|
|
244
290
|
return 0
|
|
245
291
|
fi
|
|
292
|
+
case "$source_mode" in
|
|
293
|
+
live) source_mode="ccswitch-live" ;;
|
|
294
|
+
legacy) source_mode="ccswitch" ;;
|
|
295
|
+
*) source_mode="file" ;;
|
|
296
|
+
esac
|
|
246
297
|
# TSV:event\tscript(不去重、保留零事件——计数与去重交给下游 jq 聚合)
|
|
247
298
|
pairs=$(jq -r '.hooks as $h |
|
|
248
299
|
["SessionStart","SessionEnd","UserPromptSubmit","PreToolUse","PostToolUse",
|
|
@@ -250,9 +301,7 @@ adapter_check_detail() {
|
|
|
250
301
|
$events[] as $e |
|
|
251
302
|
$h[$e][]?.hooks[]?.command // empty |
|
|
252
303
|
select(contains("tmux-ai-status")) | split(" ")[0] | "\($e)\t\(.)"' "$src" 2>/dev/null)
|
|
253
|
-
|
|
254
|
-
rm -f "$src"
|
|
255
|
-
fi
|
|
304
|
+
[ -z "$tmp" ] || rm -f "$tmp"
|
|
256
305
|
# TSV:event\tscript\talive(0|1)。bash 3.2 无关联数组,逐行展开后由 jq 一次聚合。
|
|
257
306
|
rows=""
|
|
258
307
|
while IFS= read -r line; do
|
|
@@ -264,7 +313,7 @@ adapter_check_detail() {
|
|
|
264
313
|
done <<EOF
|
|
265
314
|
$pairs
|
|
266
315
|
EOF
|
|
267
|
-
jq -R -s --arg mode "$
|
|
316
|
+
jq -R -s --arg mode "$source_mode" --arg events "$_CLAUDE_EVENTS" '
|
|
268
317
|
(reduce ($events | split(" ")[]) as $e
|
|
269
318
|
({}; .[$e] = {registered: 0, alive: 0, scriptPaths: []})) as $base
|
|
270
319
|
| reduce (split("\n") | .[] | select(length > 0) | split("\t")) as $r (
|
|
@@ -279,68 +328,90 @@ EOF
|
|
|
279
328
|
' <<<"$rows"
|
|
280
329
|
}
|
|
281
330
|
|
|
282
|
-
# 安装:合并式更新,保留他人(masko 等)注册的 hook。
|
|
283
|
-
#
|
|
331
|
+
# 安装:合并式更新,保留他人(masko 等)注册的 hook。
|
|
332
|
+
# legacy 模式只写库(~/.claude/settings.json 与实例文件都会被旧版 cc-switch 覆盖);
|
|
333
|
+
# live 模式(4.0.2+)写 ~/.claude/settings.json —— 新版 cc-switch 以它为共享真源,
|
|
334
|
+
# 实例 settings 在 switch/漂移收敛时从该文件重新派生。
|
|
284
335
|
adapter_install_hooks() {
|
|
285
336
|
_install_require_jq
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
337
|
+
local mode target updated
|
|
338
|
+
mode=$(_ccswitch_mode)
|
|
339
|
+
case "$mode" in
|
|
340
|
+
legacy)
|
|
341
|
+
local tmp merged hooks_patch full_doc
|
|
342
|
+
_ccswitch_warn_gui
|
|
343
|
+
_ccswitch_dump_common tmp || return 1
|
|
344
|
+
merged=$(_claude_apply_merge "$tmp")
|
|
345
|
+
rm -f "$tmp"
|
|
346
|
+
hooks_patch=$(jq -c '{hooks: (.hooks // {})}' <<<"$merged")
|
|
347
|
+
full_doc=$(jq -c . <<<"$merged")
|
|
348
|
+
_ccswitch_upsert_hooks "$hooks_patch" "$full_doc" || return 1
|
|
349
|
+
echo "Claude hooks installed to cc-switch common config (legacy): $(_ccswitch_db)"
|
|
350
|
+
echo "Events: $_CLAUDE_EVENTS"
|
|
351
|
+
return 0
|
|
352
|
+
;;
|
|
353
|
+
live)
|
|
354
|
+
_ccswitch_warn_gui
|
|
355
|
+
target=$(_ccswitch_live_file)
|
|
356
|
+
;;
|
|
357
|
+
*)
|
|
358
|
+
target="$ADAPTER_HOOKS_FILE"
|
|
359
|
+
;;
|
|
360
|
+
esac
|
|
361
|
+
[ -f "$target" ] || _install_atomic_write '{}' "$target"
|
|
362
|
+
updated=$(_claude_apply_merge "$target")
|
|
363
|
+
_install_atomic_write "$updated" "$target"
|
|
364
|
+
echo "Claude hooks installed to $target"
|
|
304
365
|
echo "Events: $_CLAUDE_EVENTS"
|
|
305
366
|
}
|
|
306
367
|
|
|
307
368
|
# 卸载:单次 jq pipeline 移除所有指向本插件的 hooks,保留他人条目。
|
|
308
|
-
#
|
|
369
|
+
# legacy 模式:剥离前后对照生成 patch(被清空的事件键置 null = RFC 7396 删键),
|
|
309
370
|
# 顶层其他字段(model/env/…)由 json_patch 天然保留。
|
|
371
|
+
# live 模式:与文件模式同一套原子写(真源就是 ~/.claude/settings.json)。
|
|
310
372
|
adapter_uninstall_hooks() {
|
|
311
373
|
_install_require_jq
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
374
|
+
local mode target updated
|
|
375
|
+
mode=$(_ccswitch_mode)
|
|
376
|
+
case "$mode" in
|
|
377
|
+
legacy)
|
|
378
|
+
local tmp stripped cur_hooks final_hooks patch managed top_patch
|
|
379
|
+
_ccswitch_warn_gui
|
|
380
|
+
_ccswitch_dump_common tmp || return 1
|
|
381
|
+
cur_hooks=$(jq -c '.hooks // {}' "$tmp")
|
|
382
|
+
# 已无本插件条目 → no-op(否则会把保留的他人 hook 重写一遍)
|
|
383
|
+
managed=$(jq '[to_entries[] | select(.value | any(.hooks[]?.command | contains("tmux-ai-status")))] | length' <<<"$cur_hooks")
|
|
384
|
+
if [ "${managed:-0}" -eq 0 ]; then
|
|
385
|
+
rm -f "$tmp"
|
|
386
|
+
echo "Claude hooks: nothing to uninstall (cc-switch common config)"
|
|
387
|
+
return 0
|
|
388
|
+
fi
|
|
389
|
+
stripped=$(_claude_apply_strip "$tmp")
|
|
390
|
+
final_hooks=$(jq -c '.hooks // {}' <<<"$stripped")
|
|
320
391
|
rm -f "$tmp"
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
echo "Claude hooks
|
|
392
|
+
patch=$(jq -cn --argjson cur "$cur_hooks" --argjson fin "$final_hooks" '
|
|
393
|
+
reduce ($cur | keys[]) as $k ({};
|
|
394
|
+
if ($fin | has($k)) then .[$k] = $fin[$k] else .[$k] = null end)')
|
|
395
|
+
if [ "$patch" = "{}" ]; then
|
|
396
|
+
echo "Claude hooks: nothing to uninstall (cc-switch common config)"
|
|
397
|
+
return 0
|
|
398
|
+
fi
|
|
399
|
+
# patch 是事件级键,需包一层 {hooks: …} 才是顶层 patch(json_patch 只动 hooks)
|
|
400
|
+
top_patch=$(jq -cn --argjson p "$patch" '{hooks: $p}')
|
|
401
|
+
_ccswitch_upsert_hooks "$top_patch" "$top_patch" || return 1
|
|
402
|
+
echo "Claude hooks uninstalled from cc-switch common config (legacy)"
|
|
332
403
|
return 0
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
updated=$(_claude_apply_strip "$
|
|
344
|
-
_install_atomic_write "$updated" "$
|
|
345
|
-
echo "Claude hooks uninstalled from $
|
|
404
|
+
;;
|
|
405
|
+
live)
|
|
406
|
+
_ccswitch_warn_gui
|
|
407
|
+
target=$(_ccswitch_live_file)
|
|
408
|
+
;;
|
|
409
|
+
*)
|
|
410
|
+
target="$ADAPTER_HOOKS_FILE"
|
|
411
|
+
;;
|
|
412
|
+
esac
|
|
413
|
+
[ -f "$target" ] || { echo "Claude hooks: nothing to uninstall"; return 0; }
|
|
414
|
+
updated=$(_claude_apply_strip "$target")
|
|
415
|
+
_install_atomic_write "$updated" "$target"
|
|
416
|
+
echo "Claude hooks uninstalled from $target"
|
|
346
417
|
}
|
package/src/adapters-meta.js
CHANGED
|
@@ -29,8 +29,9 @@ 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
|
|
33
|
-
//
|
|
32
|
+
// cc-switch 存在时 hooks 真源随版本变化(4.0.2+ live = ~/.claude/settings.json,
|
|
33
|
+
// 旧版 legacy = db 通用配置),文件判定失效 → 由 bash 侧 wrapper check 判定
|
|
34
|
+
// (内部按 db user_version 选真源);非 cc-switch 环境仍走本地文件计数。
|
|
34
35
|
checkViaWrapper: true,
|
|
35
36
|
ccSwitchDb: join(HOME, '.cc-switch', 'cc-switch.db'),
|
|
36
37
|
},
|
package/src/check-detail.js
CHANGED
package/src/detect.js
CHANGED
|
@@ -82,7 +82,8 @@ export async function checkEnv({ probeCommand = probe } = {}) {
|
|
|
82
82
|
pkg: 'node',
|
|
83
83
|
}));
|
|
84
84
|
|
|
85
|
-
// sqlite3
|
|
85
|
+
// sqlite3:cc-switch 环境用于读 db PRAGMA user_version 判定 live/legacy 真源;
|
|
86
|
+
// legacy 模式(真源在 db 通用配置)的装/卸/检查还依赖它写入。
|
|
86
87
|
const ccSwitchDb = join(homedir(), '.cc-switch', 'cc-switch.db');
|
|
87
88
|
if (existsSync(ccSwitchDb)) {
|
|
88
89
|
const sqlite3 = await probeCommand('sqlite3', ['--version'], /(\d+\.\d+)/);
|
package/src/help.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// `tmuxclihook help` 的静态文案。独立成模块是为了让测试可以直接 import,
|
|
2
|
+
// 而不必加载 bin/cli.js(入口文件顶层会执行参数分支,未知参数会启动 TUI)。
|
|
3
|
+
const COMMAND_WIDTH = 26;
|
|
4
|
+
|
|
5
|
+
function line(command, description) {
|
|
6
|
+
return ` ${command.padEnd(COMMAND_WIDTH)} ${description}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const HELP_TEXT = [
|
|
10
|
+
'tmuxclihook — tmux-ai-status 交互式安装器',
|
|
11
|
+
'',
|
|
12
|
+
'命令行:',
|
|
13
|
+
line('tmuxclihook', '打开交互式 TUI 安装器'),
|
|
14
|
+
line('tmuxclihook check', 'hooks 健康明细(可跟 claude|codex|opencode;exit 1 = 有缺失或漂移)'),
|
|
15
|
+
line('tmuxclihook update', '按当前安装方式(npm/volta)检查并更新到最新版本'),
|
|
16
|
+
line('tmuxclihook update --prune', '更新后删除不生效的另一份安装;已是最新时也会执行清理'),
|
|
17
|
+
line('tmuxclihook version', '显示版本(--version / -v 等价)'),
|
|
18
|
+
line('tmuxclihook help', '显示本帮助(--help / -h 等价)'),
|
|
19
|
+
'',
|
|
20
|
+
'tmux 快捷键(插件加载后生效):',
|
|
21
|
+
line('prefix + I', '打开交互式 TUI 安装器'),
|
|
22
|
+
line('prefix + C-h', '安装 Claude Code hooks'),
|
|
23
|
+
line('prefix + C-u', '卸载 Claude Code hooks'),
|
|
24
|
+
line('prefix + M-h', '安装 Codex hooks'),
|
|
25
|
+
line('prefix + M-u', '卸载 Codex hooks'),
|
|
26
|
+
line('prefix + C-p', '安装 opencode plugin'),
|
|
27
|
+
line('prefix + M-p', '卸载 opencode plugin'),
|
|
28
|
+
line('prefix + r', '重载配置(含本插件初始化)'),
|
|
29
|
+
'',
|
|
30
|
+
'prefix 默认是 Ctrl+b;若你在 tmux 配置中改过 prefix,以你的配置为准。',
|
|
31
|
+
].join('\n');
|
package/src/update.js
CHANGED
|
@@ -290,6 +290,26 @@ function warnNotEffective(log, result) {
|
|
|
290
290
|
log.error(`If you use volta, run: volta install ${PACKAGE_NAME}@latest`);
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
+
// 清理非生效副本,并在实际删除过副本后复检生效入口。升级成功路径与
|
|
294
|
+
// 「已是最新 + --prune」路径共用,避免两处逻辑漂移。清理失败只记录日志,
|
|
295
|
+
// 不影响 update 的返回值。
|
|
296
|
+
async function reconcileStaleCopies({
|
|
297
|
+
source, env, run, log, prune, isTTY, confirm,
|
|
298
|
+
checkHooks, checkTmuxConf, detectStale, verify, expectedVersion,
|
|
299
|
+
}) {
|
|
300
|
+
try {
|
|
301
|
+
const removed = await cleanupStale({
|
|
302
|
+
source, env, run, log, prune, isTTY, confirm, checkHooks, checkTmuxConf, detectStale,
|
|
303
|
+
});
|
|
304
|
+
if (removed && verify) {
|
|
305
|
+
const after = await verify(expectedVersion, env);
|
|
306
|
+
if (after.status !== 'ok') warnNotEffective(log, after);
|
|
307
|
+
}
|
|
308
|
+
} catch (err) {
|
|
309
|
+
log.error(`Cleanup skipped: ${err}`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
293
313
|
// volta 安装路径:当 PATH 上的生效入口来自 volta(shim 或 packages 镜像)
|
|
294
314
|
// 时,升级只能走 `volta install`——`npm install -g` 只写 node 工具链目录,
|
|
295
315
|
// 不会更新 volta 的 packages 镜像。此函数现在是 volta 分支的唯一升级动作,
|
|
@@ -346,6 +366,20 @@ export async function updatePackage(options = {}) {
|
|
|
346
366
|
|
|
347
367
|
if (currentVersion === latestVersion) {
|
|
348
368
|
log.log('Already up to date.');
|
|
369
|
+
|
|
370
|
+
// 版本已最新:绝不安装;仅显式 --prune 时收敛非生效副本。
|
|
371
|
+
// 未传 --prune 时保持历史行为:不探测来源、不清理、直接返回。
|
|
372
|
+
if (cleanup && prune) {
|
|
373
|
+
const effectiveSource = typeof detectSource === 'function'
|
|
374
|
+
? detectSource(env)
|
|
375
|
+
: 'unknown';
|
|
376
|
+
await reconcileStaleCopies({
|
|
377
|
+
source: effectiveSource, env, run, log, prune, isTTY, confirm,
|
|
378
|
+
checkHooks, checkTmuxConf, detectStale, verify,
|
|
379
|
+
expectedVersion: latestVersion,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
349
383
|
return true;
|
|
350
384
|
}
|
|
351
385
|
|
|
@@ -394,18 +428,11 @@ export async function updatePackage(options = {}) {
|
|
|
394
428
|
|
|
395
429
|
// 升级已确认成功后再收敛历史副本;清理失败不影响升级结果,也不改变退出码。
|
|
396
430
|
if (cleanup && result.status === 'ok') {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
if (removed && verify) {
|
|
403
|
-
const after = await verify(latestVersion, env);
|
|
404
|
-
if (after.status !== 'ok') warnNotEffective(log, after);
|
|
405
|
-
}
|
|
406
|
-
} catch (err) {
|
|
407
|
-
log.error(`Cleanup skipped: ${err}`);
|
|
408
|
-
}
|
|
431
|
+
await reconcileStaleCopies({
|
|
432
|
+
source, env, run, log, prune, isTTY, confirm,
|
|
433
|
+
checkHooks, checkTmuxConf, detectStale, verify,
|
|
434
|
+
expectedVersion: latestVersion,
|
|
435
|
+
});
|
|
409
436
|
}
|
|
410
437
|
|
|
411
438
|
return true;
|