tmuxclihook 0.1.1 → 0.2.1

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
@@ -1,23 +1,32 @@
1
1
  # tmux-ai-status 安装器
2
2
 
3
- `tmuxclihook` 是 tmux-ai-hooks-status 的交互式 TUI,负责环境侦测/修复、AI CLI 侦测、hooks 装卸修、TPM 软链校验/创建,以及完整卸载编排。启动 TUI 本身需要 Node.js 18+;它不能在 Node.js 缺失时自举运行。
3
+ `tmuxclihook` 是 tmux-ai-hooks-status 的交互式 TUI,负责环境侦测/修复、AI CLI 侦测、hooks 装卸修、tmux 集成(`.tmux.conf` 的 `run-shell` 声明校验/修复)、source 管理(生产默认 / 本地开发路径切换),以及完整卸载编排。启动 TUI 本身需要 Node.js 18+;它不能在 Node.js 缺失时自举运行。
4
4
 
5
5
  ## 运行
6
6
 
7
7
  ```bash
8
- # 完整功能:从仓库副本运行
9
- cd installer
10
- npm install
11
- npm start
8
+ # 生产:npm 全局安装,装完即完整可用(scripts/ 与 tmux 入口已打包进包内)
9
+ npm install -g tmuxclihook
10
+ tmuxclihook
11
+
12
+ # 检查 npm registry 并将全局安装更新到最新版
13
+ tmuxclihook update
12
14
 
13
15
  # 插件已经载入 tmux 后
14
16
  prefix + I
15
17
 
16
- # npm 入口(环境与 AI CLI 检查)
18
+ # npm 入口(仅环境与 AI CLI 检查,临时缓存目录不适合持久化的 hooks/tmux 写入)
17
19
  npx tmuxclihook
20
+
21
+ # 本地开发:从仓库副本运行
22
+ cd installer
23
+ npm install
24
+ npm start
18
25
  ```
19
26
 
20
- hooks 和 tmux 软链操作依赖仓库根目录中的 `scripts/` 与插件入口文件。npm 包当前适合独立运行环境/CLI 检查;完整安装操作应从仓库副本或 `prefix + I` 启动。
27
+ hooks 安装、tmux 集成写入的路径由「source」决定:环境变量 `TMUXCLIHOOK_SOURCE` > 持久化配置 `~/.config/tmuxclihook/config.json`(TUI「source 管理」菜单写入)> 包内自带 scripts/(生产默认)> 仓库根 scripts/(`cd installer && npm start` 时的开发回落)。见下方「source 管理」一节。
28
+
29
+ `tmuxclihook update` 会显示当前版本,通过 `npm view tmuxclihook version` 查询最新版;版本不同时直接执行 `npm install -g tmuxclihook@latest`,版本相同则不做修改。网络查询或安装失败时命令以非零状态退出。
21
30
 
22
31
  ## 功能
23
32
 
@@ -25,11 +34,30 @@ hooks 和 tmux 软链操作依赖仓库根目录中的 `scripts/` 与插件入
25
34
  |--------|------|
26
35
  | 环境检查 | 侦测 tmux(≥3.1) / jq / bash(任意版本) / node(≥18),显示 `✓ ready`、`✗ missing`、`⚠ outdated`,并在确认后安全修复 |
27
36
  | 侦测 AI CLI | 扫描 Claude Code / Codex / opencode,显示版本、最低版本和 hooks 状态;不自动安装或升级 AI CLI |
28
- | 安装 hooks | 选工具(claude/codex/opencode/全部)→ 调 `scripts/install-<tool>-hooks.sh` |
37
+ | 安装 hooks | 选工具(claude/codex/opencode/全部)→ 调 `scripts/install-<tool>-hooks.sh`(路径随当前 source 解析) |
29
38
  | 卸载 hooks | 对称卸载本插件 hooks,保留其他工具注册的 hook |
30
39
  | 修复 hooks | 完整性检查 → 缺失则重装 |
31
- | tmux 软链 | 校验 `~/.tmux/plugins/` 是否有指向本仓库的软链(含旧名),缺失可创建;创建后若 `~/.tmux.conf` 未声明插件,可选追加 `@plugin` 并 `tmux source-file` 重载 |
32
- | 完整卸载插件 | 停止 Codex monitor、清除聚合状态 `@ai_all_status`、删除插件软链(含旧名,不删仓库);`.tmux.conf` 删声明与 `tmux kill-server` 因破坏性仅打印手动指引 |
40
+ | tmux 集成 | 校验 `~/.tmux.conf` 是否已有指向当前 source 的 `run-shell` 声明;缺失则追加,路径过期(例如切换过 source)则原地替换,随后 `tmux source-file` 重载;不依赖 TPM 软链(历史遗留软链仅作提示,清理走「完整卸载插件」) |
41
+ | source 管理 | 查看当前生效 source(来源 + 失效警告);设置本地仓库路径用于开发调试;清除持久化配置恢复默认 |
42
+ | 完整卸载插件 | 停止 Codex monitor、清除聚合状态 `@ai_all_status`、清理历史遗留软链(不删仓库);`.tmux.conf` 删声明与 `tmux kill-server` 因破坏性仅打印手动指引 |
43
+
44
+ ## Source 管理(生产默认 / 本地调试切换)
45
+
46
+ `npm i -g` 装完后,hooks 安装与 tmux 集成默认使用包内自带的 `scripts/`(由 `npm publish` 前的 `prepack` 步骤从仓库根复制打包进来,见「架构」一节),开箱即用,无需仓库 checkout。
47
+
48
+ 调试本仓库的新功能时,无需重新 `npm publish`,把 source 指向本地 checkout 即可让改动立即生效:
49
+
50
+ ```bash
51
+ # 一次性:环境变量优先级最高
52
+ TMUXCLIHOOK_SOURCE=~/work/home/tmux-claude-hooks-status tmuxclihook
53
+
54
+ # 持久化:写入 ~/.config/tmuxclihook/config.json,之后每次运行 tmuxclihook 都生效
55
+ # (TUI 内「source 管理 → 设置本地开发路径」,或直接编辑该文件)
56
+ ```
57
+
58
+ 指向的路径必须包含 `scripts/` 子目录才会被采纳,否则该级解析会被跳过并给出警告,回落到下一级(配置文件 → 包内自带 → 仓库根)。
59
+
60
+ **切换 source 不会自动重写已经写入的 hooks 配置或 `.tmux.conf`**——两者记录的是绝对路径快照,切换后需要重新执行一次「安装 hooks」与「tmux 集成」,新路径才会生效。在 TUI「source 管理」菜单选择"清除并恢复默认"可删除持久化配置,回到包内自带/仓库回落,同样需要重新执行安装动作才会生效。
33
61
 
34
62
  ## 环境自动修复规则
35
63
 
@@ -47,6 +75,12 @@ Doctor 只通过 Homebrew 执行明确且可验证的操作:
47
75
 
48
76
  TUI 不重实现 bash hooks 逻辑——hooks 装/卸经 `execa` 调用薄 wrapper `scripts/install-<tool>-hooks.sh`(内部走 `scripts/adapters/<tool>.sh`)。工具元数据(bin/minVersion/hooksFile)在 `src/adapters-meta.js`,与 bash adapter 一一对应。
49
77
 
78
+ `src/adapters-meta.js` 的 `REPO_ROOT`/`SCRIPTS_DIR`/`TMUX_ENTRY` 不再是写死的相对路径,而是委托 `src/source.js` 的三级解析(`resolveSource()`)。这些导出是 ES module 的 live binding:`refreshSource()` 重新赋值后,所有下游 `import { REPO_ROOT } ...` 的调用方自动看到新值,无需重启进程——这是 TUI 内「source 管理」切换路径后立即生效的机制。
79
+
80
+ npm 打包时(`npm publish` / `npm pack`),`package.json` 的 `prepack` 钩子(`tools/prepack-bundle.js`)把仓库根的 `scripts/` 与 `tmux-ai-hooks-status.tmux` 复制进 `installer/`(保留可执行位,排除隐藏文件),使发布的包自包含;`postpack` 钩子(`tools/prepack-clean.js`)随后清理这些复制产物,保持仓库工作区干净(复制产物已在根 `.gitignore` 中忽略,不会误入 git)。
81
+
82
+ tmux 集成不再依赖 TPM 软链创建:`src/tmuxconf.js` 的 `ensureTmuxIntegration()` 直接在 `~/.tmux.conf` 写入/替换一行 `run-shell '<TMUX_ENTRY>'`;`src/symlink.js` 仅保留软链**检测**(`checkSymlink()`,用于提示历史遗留安装可清理)与 `.tmux.conf` 集成状态检测(`checkTmuxConf()`)。
83
+
50
84
  环境职责分离:
51
85
 
52
86
  - `src/detect.js`:只探测并分类 `ready` / `missing` / `outdated`;
@@ -62,10 +96,10 @@ npm run check
62
96
  npm test
63
97
  ```
64
98
 
65
- 测试通过依赖注入模拟 Homebrew,不会真的执行安装、升级或网络操作。
99
+ 测试通过依赖注入模拟 Homebrew、文件系统路径(`source.js`/`tmuxconf.js` 均支持注入 `configFile`/`path` 等参数),不会真的执行安装、升级或网络操作,也不会触碰真实的 `~/.tmux.conf` / `~/.config/tmuxclihook/`。
66
100
 
67
101
  ## 发布(npm)
68
102
 
69
- 由 GitHub Actions 自动发布:push 到 `main` 且 `installer/package.json` 的 `version` 比 npm 上已发布版本新时,`.github/workflows/publish-installer.yml` 自动执行 `npm publish --provenance --access public`、打 `installer-v<version>` tag 并创建 Release。
103
+ 由 GitHub Actions 自动发布:push 到 `main` 且 `installer/package.json` 的 `version` 比 npm 上已发布版本新时(改动涉及 `installer/**`、`scripts/**` 或根 `tmux-ai-hooks-status.tmux` 均会触发工作流),`.github/workflows/publish-installer.yml` 自动执行 `npm publish --provenance --access public`(`prepack`/`postpack` 自动打包/清理仓库根脚本)、打 `installer-v<version>` tag 并创建 Release。
70
104
 
71
105
  发布前置:仓库 Settings → Secrets 配置 `NPM_TOKEN`(npm automation token)。手动发布版本:修改 `package.json` 的 `version` 后提交并 push 到 main。
package/bin/cli.js CHANGED
@@ -1,18 +1,33 @@
1
1
  #!/usr/bin/env node
2
2
  // cli.js — tmux-ai-status 交互式安装 TUI(@clack/prompts)
3
3
  //
4
- // 主菜单:环境检查 / 侦测 CLI / 安装 / 卸载 / 修复 hooks / tmux 软链。
4
+ // 主菜单:环境检查 / 侦测 CLI / 安装 / 卸载 / 修复 hooks / tmux 集成 / source 管理。
5
5
  // 实际 hooks 逻辑委托 bash wrapper(scripts/install-<tool>-hooks.sh)。
6
+ //
7
+ // "source" 决定 scripts/ 与 tmux 入口来自哪里:环境变量 TMUXCLIHOOK_SOURCE >
8
+ // 持久化配置 ~/.config/tmuxclihook/config.json > 包内自带(生产默认)> 仓库根
9
+ // (开发回落)。见 src/source.js 与 docs/2026-07-15-npm-global-install-plan.md。
6
10
 
11
+ import { homedir } from 'node:os';
7
12
  import * as p from '@clack/prompts';
8
13
  import { checkEnv, detectClis, interactiveShell } from '../src/detect.js';
9
14
  import { planBrewRemediation, runBrewAction } from '../src/env.js';
10
15
  import { installHooks, uninstallHooks, repairHooks } from '../src/hooks.js';
11
- import { checkSymlink, createSymlink, checkTmuxConf } from '../src/symlink.js';
12
- import { appendPluginDeclaration, reloadTmux } from '../src/tmuxconf.js';
16
+ import { checkSymlink, checkTmuxConf } from '../src/symlink.js';
17
+ import { ensureTmuxIntegration, reloadTmux } from '../src/tmuxconf.js';
13
18
  import { stopMonitor, clearAggregateStatus, removeSymlinks } from '../src/purge.js';
14
- import { TOOLS, repoExists, REPO_ROOT } from '../src/adapters-meta.js';
19
+ import {
20
+ TOOLS,
21
+ repoExists,
22
+ REPO_ROOT,
23
+ TMUX_ENTRY,
24
+ SOURCE_ORIGIN,
25
+ SOURCE_WARNINGS,
26
+ refreshSource,
27
+ } from '../src/adapters-meta.js';
28
+ import { setSource, clearSource, getPersistedSource, originLabel } from '../src/source.js';
15
29
  import { note, padDisplay } from '../src/tui.js';
30
+ import { updatePackage } from '../src/update.js';
16
31
 
17
32
  const OK = '✓';
18
33
  const NO = '✗';
@@ -153,44 +168,128 @@ async function runHookAction(action, verb) {
153
168
  }
154
169
  }
155
170
 
156
- async function symlinkMenu() {
157
- const st = checkSymlink();
171
+ // tmux 集成:不再依赖 TPM 软链,直接检查/修复 .tmux.conf 里指向当前 source
172
+ // 的 run-shell 声明。历史遗留的 ~/.tmux/plugins/ 软链仅作提示,清理走「完整卸载插件」。
173
+ // 确认 + 应用「确保 tmux 集成」,被「tmux 集成」菜单与启动时的主动检测共用。
174
+ async function confirmAndApplyTmuxIntegration(conf) {
175
+ const go = await p.confirm({
176
+ message: conf.referenced
177
+ ? '.tmux.conf 中的声明指向过期路径(例如切换了 source),更新为当前路径并重载?'
178
+ : '.tmux.conf 未集成本插件,追加 run-shell 声明并重载?',
179
+ });
180
+ if (p.isCancel(go) || !go) return false;
181
+
182
+ const r = ensureTmuxIntegration();
183
+ if (!r.ok) { p.log.error(`${NO} 写入 .tmux.conf 失败:${r.output}`); return false; }
184
+ if (r.action === 'created') p.log.success(`${OK} 已创建 ${r.path} 并写入声明`);
185
+ else if (r.action === 'appended') p.log.success(`${OK} 已追加声明到 ${r.path}`);
186
+ else if (r.action === 'replaced') p.log.success(`${OK} 已更新 ${r.path} 中的过期路径`);
187
+ else p.log.info('无需变更');
188
+
189
+ const rl = await reloadTmux();
190
+ if (rl.ok) p.log.success(`${OK} 已重载 tmux 配置`);
191
+ else p.log.warn(`重载失败(tmux 可能未运行):${rl.output}\n请手动执行 tmux source-file ~/.tmux.conf`);
192
+ return true;
193
+ }
194
+
195
+ async function tmuxIntegrationMenu() {
196
+ const symlinkSt = checkSymlink();
158
197
  const conf = checkTmuxConf();
159
198
  const lines = [
160
- ` 软链 ${PLUGIN_LINE(st)}`,
161
- ` .tmux.conf: ${conf.exists ? (conf.referenced ? `${OK} 已引用插件` : `${NO} 未引用(需手动加 @plugin 或 run)`) : `${NO} 不存在`}`,
199
+ ` 加载方式:run-shell '${TMUX_ENTRY}'`,
200
+ ` .tmux.conf:${confStatusLine(conf)}`,
162
201
  ];
163
- note(lines.join('\n'), 'tmux 软链');
164
-
165
- // 步骤 1:软链缺失/错误则询问创建
166
- if (st.status !== 'ok') {
167
- const go = await p.confirm({ message: `创建软链 ${PLUGIN_LINK_PATH()} → 仓库?` });
168
- if (p.isCancel(go) || !go) return;
169
- const r = createSymlink();
170
- if (r.ok) p.log.success(`${OK} 软链已创建:${r.from} → ${r.to}`);
171
- else { p.log.error(`${NO} 创建失败:${r.output}`); return; }
172
- } else {
173
- p.log.success('软链正常');
202
+ if (symlinkSt.status === 'ok') {
203
+ lines.push(` 历史遗留软链:检测到 ${symlinkSt.link}(新加载方式不再需要,可在「完整卸载插件」中一并清理)`);
174
204
  }
205
+ note(lines.join('\n'), 'tmux 集成');
206
+
207
+ if (conf.pointsToCurrent) {
208
+ p.log.success('已正确集成,无需操作');
209
+ return;
210
+ }
211
+
212
+ await confirmAndApplyTmuxIntegration(conf);
213
+ }
214
+
215
+ // 启动时主动检测:未集成 / 路径过期时主动提示,而非只在「tmux 集成」子菜单里被动
216
+ // 等用户发现。尤其覆盖"全新 .tmux.conf"场景——`npm i -g` 装完不会自动写入任何东西
217
+ // (没有 postinstall 钩子),必须经这一步或手动进「tmux 集成」菜单确认后才会写入。
218
+ async function maybeOfferTmuxIntegration() {
219
+ const conf = checkTmuxConf();
220
+ if (conf.pointsToCurrent) return;
221
+
222
+ p.log.warn(
223
+ conf.exists
224
+ ? '.tmux.conf 尚未集成本插件(或路径已过期),状态栏与 hooks 不会自动生效。'
225
+ : '未找到 ~/.tmux.conf,可创建一个新文件并写入本插件的加载声明。',
226
+ );
227
+ await confirmAndApplyTmuxIntegration(conf);
228
+ }
229
+
230
+ function confStatusLine(conf) {
231
+ if (!conf.exists) return `${NO} 不存在`;
232
+ if (conf.pointsToCurrent) return `${OK} 已指向当前安装`;
233
+ if (conf.referenced) return `${NO} 已引用,但路径过期或为旧式声明`;
234
+ return `${NO} 未集成`;
235
+ }
175
236
 
176
- // 步骤 2:.tmux.conf 未引用插件则询问追加声明 + reload(闭合"软链建好但没加载")
177
- if (!conf.referenced) {
178
- const decl = await p.confirm({
179
- message: `.tmux.conf 未声明插件,追加 set -g @plugin 'tmux-ai-hooks-status' 并重载?`,
237
+ // source 管理:查看当前生效 source(含来源与失效警告),设置本地开发路径,
238
+ // 或清除持久化配置恢复默认。切换后仅影响"接下来"的 hooks 安装 / tmux 集成写入,
239
+ // 已写入的旧路径需要重新执行对应操作才会更新(见 docs 计划的残留风险第 4 条)。
240
+ function currentSourceLines() {
241
+ const lines = [
242
+ ` 当前 source:${REPO_ROOT}`,
243
+ ` 来源:${originLabel(SOURCE_ORIGIN)}`,
244
+ ];
245
+ for (const w of SOURCE_WARNINGS) lines.push(` ⚠ ${w}`);
246
+ return lines;
247
+ }
248
+
249
+ async function sourceMenu() {
250
+ note(currentSourceLines().join('\n'), 'source 状态');
251
+
252
+ const persisted = getPersistedSource();
253
+ const choice = await p.select({
254
+ message: '选择操作(Esc 返回主菜单)',
255
+ options: [
256
+ { value: 'set', label: '设置本地开发路径', hint: '指向本地仓库,改代码即时生效' },
257
+ {
258
+ value: 'clear',
259
+ label: '清除并恢复默认',
260
+ hint: persisted ? `当前已保存:${persisted}` : '未设置持久化路径',
261
+ },
262
+ { value: '__back__', label: '← 返回主菜单' },
263
+ ],
264
+ });
265
+ if (p.isCancel(choice) || choice === '__back__') return;
266
+
267
+ if (choice === 'set') {
268
+ const dir = await p.text({
269
+ message: '输入本地仓库路径(需含 scripts/ 目录)',
270
+ placeholder: '~/work/home/tmux-claude-hooks-status',
180
271
  });
181
- if (p.isCancel(decl) || !decl) return;
182
- const a = appendPluginDeclaration();
183
- if (!a.ok) { p.log.error(`${NO} 写入 .tmux.conf 失败:${a.output}`); return; }
184
- if (a.appended) p.log.success(`${OK} 已追加插件声明到 ${a.path}`);
185
- else p.log.info('已引用插件,跳过写入');
186
- const rl = await reloadTmux();
187
- if (rl.ok) p.log.success(`${OK} 已重载 tmux 配置`);
188
- else p.log.warn(`重载失败(tmux 可能未运行):${rl.output}\n请手动执行 tmux source-file ~/.tmux.conf`);
272
+ if (p.isCancel(dir) || !dir) { p.log.info('已取消'); return; }
273
+ const expanded = dir.startsWith('~') ? dir.replace(/^~/, homedir()) : dir;
274
+ const r = setSource(expanded);
275
+ if (!r.ok) { p.log.error(`${NO} ${r.output}`); return; }
276
+ refreshSource();
277
+ p.log.success(`${OK} source 已切换到 ${r.source}`);
278
+ p.log.warn('需重新执行「安装 hooks」与「tmux 集成」,新路径才会写入 hooks 配置 / .tmux.conf');
279
+ return;
280
+ }
281
+
282
+ if (choice === 'clear') {
283
+ const r = clearSource();
284
+ if (!r.ok) { p.log.error(`${NO} ${r.output}`); return; }
285
+ refreshSource();
286
+ p.log.success(`${OK} 已清除,恢复默认(${originLabel(SOURCE_ORIGIN)})`);
287
+ p.log.warn('需重新执行「安装 hooks」与「tmux 集成」,默认路径才会写回 hooks 配置 / .tmux.conf');
189
288
  }
190
289
  }
191
290
 
192
291
  async function purgeMenu() {
193
- p.log.warn('完整卸载:停止 monitor、清聚合状态、删软链。hooks 请先在「卸载 hooks」菜单移除。');
292
+ p.log.warn('完整卸载:停止 monitor、清聚合状态、清理历史遗留软链。hooks 请先在「卸载 hooks」菜单移除。');
194
293
  const go = await p.confirm({ message: '继续完整卸载插件运行态?(不删仓库、不改 .tmux.conf、不 kill-server)' });
195
294
  if (p.isCancel(go) || !go) { p.log.info('已取消'); return; }
196
295
 
@@ -203,32 +302,28 @@ async function purgeMenu() {
203
302
  else p.log.warn(`清除聚合状态失败(tmux 可能未运行):${clr.output}`);
204
303
 
205
304
  const rm = removeSymlinks();
206
- if (rm.removed.length > 0) p.log.success(`${OK} 已删除软链:${rm.removed.join(', ')}`);
207
- else p.log.info('无软链可删除');
305
+ if (rm.removed.length > 0) p.log.success(`${OK} 已清理历史遗留软链:${rm.removed.join(', ')}`);
306
+ else p.log.info('无历史遗留软链需要清理');
208
307
 
209
308
  note(
210
309
  " 以下破坏性步骤请手动执行:\n" +
211
- " 1. 从 ~/.tmux.conf 删除:set -g @plugin 'tmux-ai-hooks-status'\n" +
310
+ ` 1. 从 ~/.tmux.conf 删除本插件的 run-shell 声明(形如 run-shell '.../tmux-ai-hooks-status.tmux',\n` +
311
+ " 或历史遗留的 set -g @plugin 'tmux-ai-hooks-status')\n" +
212
312
  " 2. 重启 tmux server 使状态行/hooks/按键完全卸载:tmux kill-server\n" +
213
313
  " (kill-server 会关闭当前所有 session,请先保存工作)",
214
314
  '手动收尾',
215
315
  );
216
316
  }
217
317
 
218
- function PLUGIN_LINE(st) {
219
- if (st.status === 'ok') return `${OK} ${st.note || '指向本仓库'}`;
220
- if (st.status === 'missing') return `${NO} 不存在`;
221
- return `${NO} 错误(${st.reason || '指向其他位置'})`;
222
- }
223
- function PLUGIN_LINK_PATH() {
224
- return '~/.tmux/plugins/tmux-ai-hooks-status';
225
- }
226
-
227
318
  async function main() {
228
319
  p.intro('tmux-ai-status 安装器');
229
320
 
321
+ note(currentSourceLines().join('\n'), 'source 状态');
322
+
230
323
  if (!repoExists) {
231
- p.log.warn(`未定位到仓库 scripts/(期望 ${REPO_ROOT})。若通过 npx 运行,请在仓库内或已装插件目录下运行。`);
324
+ p.log.warn(`当前 source(${REPO_ROOT})下未找到 scripts/ 目录,hooks 安装与 tmux 集成将失败。请用「source 管理」指向一个有效仓库路径。`);
325
+ } else {
326
+ await maybeOfferTmuxIntegration();
232
327
  }
233
328
 
234
329
  while (true) {
@@ -240,8 +335,9 @@ async function main() {
240
335
  { value: 'install', label: '安装 hooks' },
241
336
  { value: 'uninstall', label: '卸载 hooks' },
242
337
  { value: 'repair', label: '修复 hooks', hint: '完整性检查 + 缺失重装' },
243
- { value: 'symlink', label: 'tmux 软链校验' },
244
- { value: 'purge', label: '完整卸载插件', hint: 'monitor stop + 清状态 + 删软链' },
338
+ { value: 'tmux', label: 'tmux 集成', hint: '校验/修复 .tmux.conf 的加载声明' },
339
+ { value: 'source', label: 'source 管理', hint: '生产默认 / 切换本地调试路径' },
340
+ { value: 'purge', label: '完整卸载插件', hint: 'monitor stop + 清状态 + 清历史软链' },
245
341
  { value: 'exit', label: '退出' },
246
342
  ],
247
343
  });
@@ -258,7 +354,8 @@ async function main() {
258
354
  else if (choice === 'install') await runHookAction(installHooks, '安装');
259
355
  else if (choice === 'uninstall') await runHookAction(uninstallHooks, '卸载');
260
356
  else if (choice === 'repair') await runHookAction(repairHooks, '修复');
261
- else if (choice === 'symlink') await symlinkMenu();
357
+ else if (choice === 'tmux') await tmuxIntegrationMenu();
358
+ else if (choice === 'source') await sourceMenu();
262
359
  else if (choice === 'purge') await purgeMenu();
263
360
  } catch (err) {
264
361
  p.log.error(String(err));
@@ -268,7 +365,16 @@ async function main() {
268
365
  p.outro('完成');
269
366
  }
270
367
 
271
- main().catch((err) => {
272
- console.error(err);
273
- process.exit(1);
274
- });
368
+ if (process.argv[2] === 'update') {
369
+ updatePackage().then((ok) => {
370
+ if (!ok) process.exitCode = 1;
371
+ }).catch((err) => {
372
+ console.error(err);
373
+ process.exitCode = 1;
374
+ });
375
+ } else {
376
+ main().catch((err) => {
377
+ console.error(err);
378
+ process.exit(1);
379
+ });
380
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmuxclihook",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "交互式 TUI 安装器:环境侦测、AI CLI 侦测、tmux-ai-status hooks 装/卸/修、TPM 软链校验",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,12 +9,16 @@
9
9
  "files": [
10
10
  "bin",
11
11
  "src",
12
+ "scripts",
13
+ "tmux-ai-hooks-status.tmux",
12
14
  "README.md"
13
15
  ],
14
16
  "scripts": {
15
- "check": "for f in bin/cli.js src/*.js; do node --check \"$f\"; done && echo 'all js syntax OK'",
17
+ "check": "for f in bin/cli.js src/*.js tools/*.js; do node --check \"$f\"; done && echo 'all js syntax OK'",
16
18
  "start": "node bin/cli.js",
17
- "test": "node --test"
19
+ "test": "node --test",
20
+ "prepack": "node tools/prepack-bundle.js",
21
+ "postpack": "node tools/prepack-clean.js"
18
22
  },
19
23
  "engines": {
20
24
  "node": ">=18"
@@ -0,0 +1,127 @@
1
+ #!/bin/bash
2
+ # adapters/claude.sh — Claude Code 工具适配器
3
+ #
4
+ # 由 lib-tmux-ai-status.sh 在确定 TOOL_ID=claude 后 source。声明 Claude Code 与其他
5
+ # AI CLI 的差异;核心引擎读本文件暴露的变量/函数分派,不再写 case "$TOOL_ID"。
6
+ #
7
+ # 契约(每个 adapter 必须定义):
8
+ # ADAPTER_EVENTS 该工具触发的事件集合(空格分隔)
9
+ # ADAPTER_PROCESS_NAMES 进程名(basename)集合,供 _pane_has_ai_process 识别
10
+ # ADAPTER_HOOKS_FILE hooks 配置文件绝对路径
11
+ # ADAPTER_HAS_SESSION_END "true"|"false" 是否有 SessionEnd 事件
12
+ # ADAPTER_SESSION_START_TIMING "immediate"|"deferred" SessionStart 时机
13
+ # ADAPTER_HOLD_UNMATCHED_PERMISSION "true"|"false" 无请求 ID 时是否保守保持审批态
14
+ # ADAPTER_INSTALLER install 脚本绝对路径(供自修复/TUI 调用)
15
+ # adapter_check_integrity() hooks 是否完整注册本插件(返回 0=完整)
16
+ # adapter_install_hooks() 安装 hooks(合并式,保留他人 hook)
17
+ # adapter_uninstall_hooks() 卸载本插件 hooks
18
+ #
19
+ # install/uninstall 依赖 lib-install-hooks.sh 的 _install_require_jq/_install_atomic_write,
20
+ # 仅在被 install wrapper source 时可用;事件路径 source 本文件只定义函数不调用,零开销。
21
+
22
+ # Claude Code 注册 10 个事件,全部 async(PermissionRequest 例外 sync)。
23
+ ADAPTER_EVENTS="SessionStart SessionEnd UserPromptSubmit PreToolUse PostToolUse PostToolUseFailure PermissionRequest Notification Stop StopFailure"
24
+
25
+ # 前台进程名(BFS pane 进程树匹配 basename)
26
+ ADAPTER_PROCESS_NAMES="claude"
27
+
28
+ # hooks 配置:~/.claude/settings.json(CLAUDE_CONFIG_DIR 可覆盖)
29
+ ADAPTER_HOOKS_FILE="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json"
30
+
31
+ # Claude Code 有 SessionEnd 事件,退出即发 hook → 状态可主动清。
32
+ ADAPTER_HAS_SESSION_END="true"
33
+ # SessionStart 在 CLI 启动时立即触发(对比 codex 延迟到首个 turn)。
34
+ ADAPTER_SESSION_START_TIMING="immediate"
35
+ # Claude 当前串行执行工具,完成事件可解除无 ID 的审批态。
36
+ ADAPTER_HOLD_UNMATCHED_PERMISSION="false"
37
+
38
+ # 自修复/TUI 调用的安装脚本
39
+ ADAPTER_INSTALLER="${_LIB_DIR}/install-claude-hooks.sh"
40
+
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
+ # Claude 事件集合(含同步事件 PermissionRequest)。install/uninstall 用。
57
+ _CLAUDE_EVENTS="SessionStart SessionEnd UserPromptSubmit PreToolUse PostToolUse PostToolUseFailure PermissionRequest Notification Stop StopFailure"
58
+
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" '
69
+ ["SessionStart","SessionEnd","UserPromptSubmit","PreToolUse","PostToolUse",
70
+ "PostToolUseFailure","PermissionRequest","Notification","Stop","StopFailure"] as $events |
71
+ ["PermissionRequest"] as $sync_events |
72
+ reduce $events[] as $event (
73
+ .;
74
+ ($tool_cmd + " " + $event) as $hook_cmd |
75
+ .hooks //= {} |
76
+ .hooks[$event] //= [] |
77
+ .hooks[$event] |= (
78
+ map(
79
+ .hooks = [
80
+ .hooks[]
81
+ | select(.command | contains("tmux-powerline-claude-status") | not)
82
+ | select(.command | contains("tmux-claude-status") | not)
83
+ | select(
84
+ (.command | contains("tmux-ai-status") | not)
85
+ or (.command | startswith($dev_script))
86
+ )
87
+ ]
88
+ | select(.hooks | length > 0)
89
+ )
90
+ ) |
91
+ if ([.hooks[$event][]?.hooks[]?.command] | index($hook_cmd)) == null then
92
+ .hooks[$event] += [{
93
+ "hooks": [{
94
+ "async": (($event | IN($sync_events[])) | not),
95
+ "command": $hook_cmd,
96
+ "type": "command"
97
+ }],
98
+ "matcher": ""
99
+ }]
100
+ else . end
101
+ )
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
+ }
107
+
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" '
115
+ .hooks |= if . then
116
+ [. | to_entries[] |
117
+ .value |= [
118
+ .[] | .hooks = [.hooks[] | select(.command | startswith($hook_script) | not)]
119
+ | select(.hooks | length > 0)
120
+ ]
121
+ | select(.value | length > 0)
122
+ ] | from_entries
123
+ else . end
124
+ ' "$ADAPTER_HOOKS_FILE")
125
+ _install_atomic_write "$updated" "$ADAPTER_HOOKS_FILE"
126
+ echo "Claude hooks uninstalled from $ADAPTER_HOOKS_FILE"
127
+ }
@@ -0,0 +1,117 @@
1
+ #!/bin/bash
2
+ # adapters/codex.sh — Codex CLI 工具适配器(需 codex ≥ 0.144)
3
+ #
4
+ # 由 lib-tmux-ai-status.sh 在确定 TOOL_ID=codex 后 source。契约见 adapters/claude.sh 头注释。
5
+ #
6
+ # ⚠️ Codex 0.144 与 Claude 的关键差异(本 adapter 声明的语义):
7
+ # - 无 SessionEnd 事件:codex 退出无任何 hook,pane 内 shell 仍活时 pane-exited 也不触发。
8
+ # 退出状态由 tmux-ai-monitor 跟踪已见 Codex 的 pane,进程消失后精确清除。
9
+ # - SessionStart 延迟:codex 的 SessionStart hook 延迟到首个 turn 才发(上游
10
+ # run_pending_session_start_hooks),空闲蹲提示符时无事件。空窗期靠 build_all_status
11
+ # 用 pane_current_command 补 idle `-`。
12
+ # - hook trust 门禁:user 来源 hook 需 hooks.state.<key>.trusted_hash 匹配才运行;
13
+ # hash 无法在 bash 复现,靠用户首启 codex TUI 点 "Trust all and continue"。
14
+
15
+ # Codex 只触发 6 个事件(无 SessionEnd/Notification/PostToolUseFailure/StopFailure)。
16
+ ADAPTER_EVENTS="SessionStart UserPromptSubmit PreToolUse PermissionRequest PostToolUse Stop"
17
+
18
+ # 前台进程名(macOS 上 codex 二进制常为 codex-aarch64-* 等,basename 前缀匹配见
19
+ # _pane_has_ai_process 的 case codex*))
20
+ ADAPTER_PROCESS_NAMES="codex"
21
+
22
+ # hooks 配置:~/.codex/hooks.json(CODEX_HOME 可覆盖)
23
+ ADAPTER_HOOKS_FILE="${CODEX_HOME:-$HOME/.codex}/hooks.json"
24
+
25
+ # Codex 无 SessionEnd → 退出由 tmux-ai-monitor 做进程检测
26
+ ADAPTER_HAS_SESSION_END="false"
27
+ # SessionStart 延迟到首个 turn
28
+ ADAPTER_SESSION_START_TIMING="deferred"
29
+ # PermissionRequest 不含 tool_use_id,而 Codex 支持并行工具。用 sentinel 保守保持 `!`,
30
+ # 直到 Stop/UserPromptSubmit/SessionStart 清理,避免其他工具完成时漏报待审批。
31
+ ADAPTER_HOLD_UNMATCHED_PERMISSION="true"
32
+ # request_permissions 直接弹出 Codex 权限 UI,但上游不会发送 PermissionRequest hook。
33
+ # 它仍发送可配对的 PreToolUse/PostToolUse,因此可在 PreToolUse 阶段精确进入审批态。
34
+ ADAPTER_PERMISSION_PRE_TOOL_NAMES="request_permissions"
35
+
36
+ # 自修复/TUI 调用的安装脚本
37
+ ADAPTER_INSTALLER="${_LIB_DIR}/install-codex-hooks.sh"
38
+
39
+ # 完整性检查:hooks.json 是否含 6 个本插件 command(0.144+)。
40
+ adapter_check_integrity() {
41
+ [ -f "$ADAPTER_HOOKS_FILE" ] || return 1
42
+ local n
43
+ n=$(jq '[.hooks[]?[]?.hooks[]?.command // empty
44
+ | select(contains("tmux-ai-status codex"))] | length' \
45
+ "$ADAPTER_HOOKS_FILE" 2>/dev/null)
46
+ [ "${n:-0}" -ge 6 ]
47
+ }
48
+
49
+ # Codex 6 事件(全部同步——0.144 静默跳过 async=true 的 hook)
50
+ _CODEX_EVENTS="SessionStart UserPromptSubmit PreToolUse PermissionRequest PostToolUse Stop"
51
+ _CODEX_LEGACY_TOML="${CODEX_HOME:-$HOME/.codex}/hooks.toml"
52
+
53
+ # 从每个事件数组剔除本插件 group(command 含 tmux-ai-status),事件/hooks 清空后删键。
54
+ _codex_strip_managed() {
55
+ jq '
56
+ def clean_event:
57
+ map(select(
58
+ (.hooks // []) | any(.type == "command" and (.command // "" | contains("tmux-ai-status"))) | not
59
+ ));
60
+ if .hooks then
61
+ .hooks |= (with_entries(.value |= clean_event) | with_entries(select(.value | length > 0)))
62
+ else . end
63
+ | if (.hooks // {}) == {} then del(.hooks) else . end
64
+ '
65
+ }
66
+
67
+ _codex_read_existing() {
68
+ if [ -f "$ADAPTER_HOOKS_FILE" ] && jq empty "$ADAPTER_HOOKS_FILE" >/dev/null 2>&1; then
69
+ cat "$ADAPTER_HOOKS_FILE"
70
+ else
71
+ echo '{}'
72
+ fi
73
+ }
74
+
75
+ # 安装:改写 hooks.json(剥离旧 managed group 后为每事件追加本插件 group),保留他人 hook。
76
+ # 清理旧 hooks.toml(0.144 不再读取)。trust 无法在 bash 复现,靠用户首启 codex TUI 授信。
77
+ adapter_install_hooks() {
78
+ _install_require_jq
79
+ mkdir -p "$(dirname "$ADAPTER_HOOKS_FILE")" 2>/dev/null || true
80
+ [ -f "$_CODEX_LEGACY_TOML" ] && rm -f "$_CODEX_LEGACY_TOML" && echo "移除旧 hooks.toml(0.144 不再读取)"
81
+ local hook_script="${_LIB_DIR}/tmux-ai-status"
82
+ local new_json
83
+ new_json=$(_codex_read_existing | _codex_strip_managed | jq \
84
+ --arg script "$hook_script" \
85
+ --arg events "$_CODEX_EVENTS" '
86
+ .hooks //= {}
87
+ | reduce ($events | split(" ")[]) as $ev (.;
88
+ .hooks[$ev] = ((.hooks[$ev] // []) + [{
89
+ hooks: [{ type: "command", command: ($script + " codex " + $ev), timeout: 5 }]
90
+ }])
91
+ )
92
+ ')
93
+ _install_atomic_write "$new_json" "$ADAPTER_HOOKS_FILE"
94
+ echo "Codex hooks installed to $ADAPTER_HOOKS_FILE"
95
+ echo "Events: $_CODEX_EVENTS"
96
+ echo ""
97
+ echo "⚠️ 下次启动 codex 时 TUI 会提示 \"Hooks need review\",选择 \"Trust all and continue\" 授信一次即可永久生效。"
98
+ }
99
+
100
+ # 卸载:剥离本插件 group,剩空对象则删文件,否则写回。清理旧 toml。
101
+ adapter_uninstall_hooks() {
102
+ _install_require_jq
103
+ [ -f "$_CODEX_LEGACY_TOML" ] && rm -f "$_CODEX_LEGACY_TOML" && echo "移除旧 hooks.toml"
104
+ if [ -f "$ADAPTER_HOOKS_FILE" ]; then
105
+ local stripped
106
+ stripped=$(_codex_read_existing | _codex_strip_managed)
107
+ if [ "$(printf '%s' "$stripped" | jq -c .)" = "{}" ]; then
108
+ rm -f "$ADAPTER_HOOKS_FILE"
109
+ echo "Codex hooks uninstalled (removed empty $ADAPTER_HOOKS_FILE)"
110
+ else
111
+ _install_atomic_write "$stripped" "$ADAPTER_HOOKS_FILE"
112
+ echo "Codex hooks uninstalled from $ADAPTER_HOOKS_FILE (preserved external content)"
113
+ fi
114
+ else
115
+ echo "Codex hooks: nothing to uninstall ($ADAPTER_HOOKS_FILE not found)"
116
+ fi
117
+ }