tmuxclihook 0.1.1 → 0.2.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 +46 -12
- package/bin/cli.js +158 -52
- package/package.json +7 -3
- package/scripts/adapters/claude.sh +127 -0
- package/scripts/adapters/codex.sh +114 -0
- package/scripts/adapters/opencode.sh +169 -0
- package/scripts/install-claude-hooks.sh +19 -0
- package/scripts/install-codex-hooks.sh +19 -0
- package/scripts/install-opencode-hooks.sh +19 -0
- package/scripts/lib-install-hooks.sh +26 -0
- package/scripts/lib-tmux-ai-log.sh +81 -0
- package/scripts/lib-tmux-ai-status.sh +521 -0
- package/scripts/tmux-ai-esc +24 -0
- package/scripts/tmux-ai-monitor +190 -0
- package/scripts/tmux-ai-status +256 -0
- package/src/adapters-meta.js +90 -53
- package/src/purge.js +7 -1
- package/src/source.js +140 -0
- package/src/symlink.js +20 -19
- package/src/tmuxconf.js +66 -21
- package/src/update.js +39 -0
- package/tmux-ai-hooks-status.tmux +114 -0
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 装卸修、
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
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
|
|
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
|
|
32
|
-
|
|
|
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
|
|
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,
|
|
12
|
-
import {
|
|
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 {
|
|
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
|
-
|
|
157
|
-
|
|
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
|
-
`
|
|
161
|
-
` .tmux.conf
|
|
199
|
+
` 加载方式:run-shell '${TMUX_ENTRY}'`,
|
|
200
|
+
` .tmux.conf:${confStatusLine(conf)}`,
|
|
162
201
|
];
|
|
163
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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(
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
if (
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
|
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}
|
|
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
|
-
|
|
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(
|
|
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: '
|
|
244
|
-
{ value: '
|
|
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 === '
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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.
|
|
3
|
+
"version": "0.2.0",
|
|
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,114 @@
|
|
|
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
|
+
|
|
33
|
+
# 自修复/TUI 调用的安装脚本
|
|
34
|
+
ADAPTER_INSTALLER="${_LIB_DIR}/install-codex-hooks.sh"
|
|
35
|
+
|
|
36
|
+
# 完整性检查:hooks.json 是否含 6 个本插件 command(0.144+)。
|
|
37
|
+
adapter_check_integrity() {
|
|
38
|
+
[ -f "$ADAPTER_HOOKS_FILE" ] || return 1
|
|
39
|
+
local n
|
|
40
|
+
n=$(jq '[.hooks[]?[]?.hooks[]?.command // empty
|
|
41
|
+
| select(contains("tmux-ai-status codex"))] | length' \
|
|
42
|
+
"$ADAPTER_HOOKS_FILE" 2>/dev/null)
|
|
43
|
+
[ "${n:-0}" -ge 6 ]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Codex 6 事件(全部同步——0.144 静默跳过 async=true 的 hook)
|
|
47
|
+
_CODEX_EVENTS="SessionStart UserPromptSubmit PreToolUse PermissionRequest PostToolUse Stop"
|
|
48
|
+
_CODEX_LEGACY_TOML="${CODEX_HOME:-$HOME/.codex}/hooks.toml"
|
|
49
|
+
|
|
50
|
+
# 从每个事件数组剔除本插件 group(command 含 tmux-ai-status),事件/hooks 清空后删键。
|
|
51
|
+
_codex_strip_managed() {
|
|
52
|
+
jq '
|
|
53
|
+
def clean_event:
|
|
54
|
+
map(select(
|
|
55
|
+
(.hooks // []) | any(.type == "command" and (.command // "" | contains("tmux-ai-status"))) | not
|
|
56
|
+
));
|
|
57
|
+
if .hooks then
|
|
58
|
+
.hooks |= (with_entries(.value |= clean_event) | with_entries(select(.value | length > 0)))
|
|
59
|
+
else . end
|
|
60
|
+
| if (.hooks // {}) == {} then del(.hooks) else . end
|
|
61
|
+
'
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
_codex_read_existing() {
|
|
65
|
+
if [ -f "$ADAPTER_HOOKS_FILE" ] && jq empty "$ADAPTER_HOOKS_FILE" >/dev/null 2>&1; then
|
|
66
|
+
cat "$ADAPTER_HOOKS_FILE"
|
|
67
|
+
else
|
|
68
|
+
echo '{}'
|
|
69
|
+
fi
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# 安装:改写 hooks.json(剥离旧 managed group 后为每事件追加本插件 group),保留他人 hook。
|
|
73
|
+
# 清理旧 hooks.toml(0.144 不再读取)。trust 无法在 bash 复现,靠用户首启 codex TUI 授信。
|
|
74
|
+
adapter_install_hooks() {
|
|
75
|
+
_install_require_jq
|
|
76
|
+
mkdir -p "$(dirname "$ADAPTER_HOOKS_FILE")" 2>/dev/null || true
|
|
77
|
+
[ -f "$_CODEX_LEGACY_TOML" ] && rm -f "$_CODEX_LEGACY_TOML" && echo "移除旧 hooks.toml(0.144 不再读取)"
|
|
78
|
+
local hook_script="${_LIB_DIR}/tmux-ai-status"
|
|
79
|
+
local new_json
|
|
80
|
+
new_json=$(_codex_read_existing | _codex_strip_managed | jq \
|
|
81
|
+
--arg script "$hook_script" \
|
|
82
|
+
--arg events "$_CODEX_EVENTS" '
|
|
83
|
+
.hooks //= {}
|
|
84
|
+
| reduce ($events | split(" ")[]) as $ev (.;
|
|
85
|
+
.hooks[$ev] = ((.hooks[$ev] // []) + [{
|
|
86
|
+
hooks: [{ type: "command", command: ($script + " codex " + $ev), timeout: 5 }]
|
|
87
|
+
}])
|
|
88
|
+
)
|
|
89
|
+
')
|
|
90
|
+
_install_atomic_write "$new_json" "$ADAPTER_HOOKS_FILE"
|
|
91
|
+
echo "Codex hooks installed to $ADAPTER_HOOKS_FILE"
|
|
92
|
+
echo "Events: $_CODEX_EVENTS"
|
|
93
|
+
echo ""
|
|
94
|
+
echo "⚠️ 下次启动 codex 时 TUI 会提示 \"Hooks need review\",选择 \"Trust all and continue\" 授信一次即可永久生效。"
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# 卸载:剥离本插件 group,剩空对象则删文件,否则写回。清理旧 toml。
|
|
98
|
+
adapter_uninstall_hooks() {
|
|
99
|
+
_install_require_jq
|
|
100
|
+
[ -f "$_CODEX_LEGACY_TOML" ] && rm -f "$_CODEX_LEGACY_TOML" && echo "移除旧 hooks.toml"
|
|
101
|
+
if [ -f "$ADAPTER_HOOKS_FILE" ]; then
|
|
102
|
+
local stripped
|
|
103
|
+
stripped=$(_codex_read_existing | _codex_strip_managed)
|
|
104
|
+
if [ "$(printf '%s' "$stripped" | jq -c .)" = "{}" ]; then
|
|
105
|
+
rm -f "$ADAPTER_HOOKS_FILE"
|
|
106
|
+
echo "Codex hooks uninstalled (removed empty $ADAPTER_HOOKS_FILE)"
|
|
107
|
+
else
|
|
108
|
+
_install_atomic_write "$stripped" "$ADAPTER_HOOKS_FILE"
|
|
109
|
+
echo "Codex hooks uninstalled from $ADAPTER_HOOKS_FILE (preserved external content)"
|
|
110
|
+
fi
|
|
111
|
+
else
|
|
112
|
+
echo "Codex hooks: nothing to uninstall ($ADAPTER_HOOKS_FILE not found)"
|
|
113
|
+
fi
|
|
114
|
+
}
|