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 +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 +117 -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 +265 -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/src/source.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// source.js — 解析当前生效的「代码来源」(scripts/ + tmux 入口所在目录)
|
|
2
|
+
//
|
|
3
|
+
// npm 全局安装后,installer/scripts 与 installer/tmux-ai-hooks-status.tmux 由
|
|
4
|
+
// prepack 从仓库根复制打包进来,作为生产默认("bundled")。开发者也可以通过
|
|
5
|
+
// 环境变量或持久化配置指向本地仓库副本进行调试,改代码立即生效,无需重新发布。
|
|
6
|
+
//
|
|
7
|
+
// 解析优先级(从高到低):
|
|
8
|
+
// 1. 环境变量 TMUXCLIHOOK_SOURCE=<绝对路径> 一次性 / CI 场景
|
|
9
|
+
// 2. 持久化配置 ~/.config/tmuxclihook/config.json TUI「source 管理」菜单写入
|
|
10
|
+
// 3. 包内自带 installer/(prepack 复制产物) 生产默认,自包含
|
|
11
|
+
// 4. 开发回落 仓库根(installer/ 的上一级) cd installer && npm start
|
|
12
|
+
//
|
|
13
|
+
// "source" 指向的是一个目录(repo root),要求其下存在 scripts/ 子目录;
|
|
14
|
+
// tmux-ai-hooks-status.tmux 若存在则一并使用,不存在也不影响 hooks 相关功能。
|
|
15
|
+
|
|
16
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
|
|
17
|
+
import { homedir } from 'node:os';
|
|
18
|
+
import { join, dirname, resolve } from 'node:path';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
|
|
21
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
|
|
23
|
+
// installer/src/source.js 的上一级即 installer/ 本身
|
|
24
|
+
const INSTALLER_DIR = resolve(__dirname, '..');
|
|
25
|
+
// installer/ 的上一级:开发布局下的仓库根
|
|
26
|
+
const DEV_REPO_ROOT = resolve(INSTALLER_DIR, '..');
|
|
27
|
+
|
|
28
|
+
export const CONFIG_DIR = join(homedir(), '.config', 'tmuxclihook');
|
|
29
|
+
export const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
30
|
+
|
|
31
|
+
// 合法 repo root 的判定标准:其下存在 scripts/ 目录
|
|
32
|
+
function isValidRepoRoot(dir) {
|
|
33
|
+
return !!dir && existsSync(join(dir, 'scripts'));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 读取持久化配置中的 source 路径。文件不存在/损坏返回 null。
|
|
37
|
+
function readConfigSource(configFile) {
|
|
38
|
+
try {
|
|
39
|
+
const raw = readFileSync(configFile, 'utf8');
|
|
40
|
+
const json = JSON.parse(raw);
|
|
41
|
+
return typeof json.source === 'string' && json.source ? json.source : null;
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 三级解析当前生效 source。返回 { repoRoot, scriptsDir, tmuxEntry, origin, warnings }
|
|
48
|
+
// origin: 'env' | 'config' | 'bundled' | 'repo'
|
|
49
|
+
//
|
|
50
|
+
// bundledDir / devRepoRoot 可注入以便测试(默认分别为 installer/ 本身与仓库根),
|
|
51
|
+
// 生产调用方不传,走真实路径。
|
|
52
|
+
export function resolveSource({
|
|
53
|
+
env = process.env,
|
|
54
|
+
configFile = CONFIG_FILE,
|
|
55
|
+
bundledDir = INSTALLER_DIR,
|
|
56
|
+
devRepoRoot = DEV_REPO_ROOT,
|
|
57
|
+
} = {}) {
|
|
58
|
+
const warnings = [];
|
|
59
|
+
|
|
60
|
+
const envSource = env.TMUXCLIHOOK_SOURCE;
|
|
61
|
+
if (envSource) {
|
|
62
|
+
const abs = resolve(envSource);
|
|
63
|
+
if (isValidRepoRoot(abs)) {
|
|
64
|
+
return buildResult(abs, 'env', warnings);
|
|
65
|
+
}
|
|
66
|
+
warnings.push(`环境变量 TMUXCLIHOOK_SOURCE=${envSource} 下未找到 scripts/ 目录,已忽略`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const configSource = readConfigSource(configFile);
|
|
70
|
+
if (configSource) {
|
|
71
|
+
const abs = resolve(configSource);
|
|
72
|
+
if (isValidRepoRoot(abs)) {
|
|
73
|
+
return buildResult(abs, 'config', warnings);
|
|
74
|
+
}
|
|
75
|
+
warnings.push(`已保存的 source(${configSource})下未找到 scripts/ 目录,已忽略`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (isValidRepoRoot(bundledDir)) {
|
|
79
|
+
return buildResult(bundledDir, 'bundled', warnings);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 开发回落:即使仓库根 scripts/ 也不存在(理论不应发生),仍返回该路径供上游报错提示
|
|
83
|
+
return buildResult(devRepoRoot, 'repo', warnings);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function buildResult(repoRoot, origin, warnings) {
|
|
87
|
+
return {
|
|
88
|
+
repoRoot,
|
|
89
|
+
scriptsDir: join(repoRoot, 'scripts'),
|
|
90
|
+
tmuxEntry: join(repoRoot, 'tmux-ai-hooks-status.tmux'),
|
|
91
|
+
origin,
|
|
92
|
+
warnings,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 持久化设置本地 source(写 ~/.config/tmuxclihook/config.json)。
|
|
97
|
+
// 会校验目标目录下是否存在 scripts/,无效路径直接拒绝,不写入。
|
|
98
|
+
export function setSource(dir, { configFile = CONFIG_FILE, configDir = CONFIG_DIR } = {}) {
|
|
99
|
+
const abs = resolve(dir);
|
|
100
|
+
if (!isValidRepoRoot(abs)) {
|
|
101
|
+
return { ok: false, output: `${abs} 下未找到 scripts/ 目录,不是合法的仓库路径` };
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
mkdirSync(configDir, { recursive: true });
|
|
105
|
+
writeFileSync(configFile, `${JSON.stringify({ source: abs }, null, 2)}\n`);
|
|
106
|
+
return { ok: true, source: abs };
|
|
107
|
+
} catch (err) {
|
|
108
|
+
return { ok: false, output: String(err) };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 清除持久化 source,恢复默认(env 优先,其次包内自带/仓库回落)。
|
|
113
|
+
export function clearSource({ configFile = CONFIG_FILE } = {}) {
|
|
114
|
+
try {
|
|
115
|
+
if (existsSync(configFile)) rmSync(configFile, { force: true });
|
|
116
|
+
return { ok: true };
|
|
117
|
+
} catch (err) {
|
|
118
|
+
return { ok: false, output: String(err) };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 供 TUI 展示:当前持久化配置里保存的 source(不做校验,仅原样返回)
|
|
123
|
+
export function getPersistedSource({ configFile = CONFIG_FILE } = {}) {
|
|
124
|
+
return readConfigSource(configFile);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function isValidSourcePath(dir) {
|
|
128
|
+
return isValidRepoRoot(dir);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const ORIGIN_LABELS = {
|
|
132
|
+
env: '环境变量 TMUXCLIHOOK_SOURCE',
|
|
133
|
+
config: '已保存的本地路径',
|
|
134
|
+
bundled: '包内自带(生产默认)',
|
|
135
|
+
repo: '仓库根(开发回落)',
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export function originLabel(origin) {
|
|
139
|
+
return ORIGIN_LABELS[origin] || origin;
|
|
140
|
+
}
|
package/src/symlink.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
// symlink.js — TPM
|
|
2
|
-
|
|
1
|
+
// symlink.js — TPM 软链校验(历史遗留兼容) + .tmux.conf 集成状态检查
|
|
2
|
+
//
|
|
3
|
+
// 主加载路径已改为 tmuxconf.js 的 run-shell(见该文件),不再依赖本文件的软链
|
|
4
|
+
// 创建。checkSymlink() 仅用于 TUI「tmux 集成」菜单里检测/清理历史遗留的
|
|
5
|
+
// ~/.tmux/plugins/ 软链;checkTmuxConf() 供 tmuxconf.js 判断 .tmux.conf 集成状态
|
|
6
|
+
// (含旧式 TPM @plugin 声明的宽松探测,以及是否已指向"当前 source"的精确匹配)。
|
|
7
|
+
import { existsSync, lstatSync, readlinkSync, readFileSync, readdirSync } from 'node:fs';
|
|
3
8
|
import { dirname, join } from 'node:path';
|
|
4
9
|
import { homedir } from 'node:os';
|
|
5
|
-
import { PLUGIN_LINK, REPO_ROOT } from './adapters-meta.js';
|
|
10
|
+
import { PLUGIN_LINK, REPO_ROOT, TMUX_ENTRY } from './adapters-meta.js';
|
|
6
11
|
|
|
7
12
|
const PLUGINS_DIR = dirname(PLUGIN_LINK);
|
|
8
13
|
|
|
@@ -46,28 +51,24 @@ export function checkSymlink() {
|
|
|
46
51
|
}
|
|
47
52
|
}
|
|
48
53
|
|
|
49
|
-
//
|
|
50
|
-
export function
|
|
51
|
-
|
|
52
|
-
mkdirSync(dirname(PLUGIN_LINK), { recursive: true });
|
|
53
|
-
// 已存在悬空软链先删
|
|
54
|
-
try { lstatSync(PLUGIN_LINK); } catch { /* not exist, ok */ }
|
|
55
|
-
symlinkSync(REPO_ROOT, PLUGIN_LINK, 'dir');
|
|
56
|
-
return { ok: true, from: PLUGIN_LINK, to: REPO_ROOT };
|
|
57
|
-
} catch (err) {
|
|
58
|
-
return { ok: false, output: err.code === 'EEXIST' ? '软链已存在' : String(err) };
|
|
59
|
-
}
|
|
54
|
+
// 构造本插件在 .tmux.conf 中的标准 run-shell 声明行
|
|
55
|
+
export function runShellDeclarationLine(tmuxEntry = TMUX_ENTRY) {
|
|
56
|
+
return `run-shell '${tmuxEntry}'`;
|
|
60
57
|
}
|
|
61
58
|
|
|
62
|
-
// 检查 ~/.tmux.conf
|
|
63
|
-
|
|
59
|
+
// 检查 ~/.tmux.conf 对本插件的集成状态:
|
|
60
|
+
// referenced 是否存在任何指向本插件的声明(新式 run-shell 或旧式 TPM @plugin,
|
|
61
|
+
// 任意路径/任意来源,宽松子串匹配,兼容历史安装)
|
|
62
|
+
// pointsToCurrent 是否已存在指向"当前 source"tmux 入口绝对路径的精确 run-shell 行
|
|
63
|
+
export function checkTmuxConf({ tmuxEntry = TMUX_ENTRY } = {}) {
|
|
64
64
|
const conf = join(homedir(), '.tmux.conf');
|
|
65
|
-
if (!existsSync(conf)) return { exists: false, referenced: false };
|
|
65
|
+
if (!existsSync(conf)) return { exists: false, referenced: false, pointsToCurrent: false };
|
|
66
66
|
try {
|
|
67
67
|
const raw = readFileSync(conf, 'utf8');
|
|
68
68
|
const referenced = /tmux-ai-hooks-status|tmux-claude-hooks-status/.test(raw);
|
|
69
|
-
|
|
69
|
+
const pointsToCurrent = raw.includes(runShellDeclarationLine(tmuxEntry));
|
|
70
|
+
return { exists: true, referenced, pointsToCurrent };
|
|
70
71
|
} catch {
|
|
71
|
-
return { exists: true, referenced: false };
|
|
72
|
+
return { exists: true, referenced: false, pointsToCurrent: false };
|
|
72
73
|
}
|
|
73
74
|
}
|
package/src/tmuxconf.js
CHANGED
|
@@ -1,39 +1,82 @@
|
|
|
1
|
-
// tmuxconf.js — 写入 .tmux.conf 的
|
|
1
|
+
// tmuxconf.js — 写入 .tmux.conf 的 run-shell 集成声明 + reload tmux
|
|
2
|
+
//
|
|
3
|
+
// 加载机制不再依赖 TPM 风格的 `@plugin` 声明与软链,而是直接一行
|
|
4
|
+
// `run-shell '<当前 source 的 tmux 入口绝对路径>'`。TMUX_ENTRY 随 source.js
|
|
5
|
+
// 的三级解析(env / 持久化配置 / 包内自带 / 开发回落)而变化,切换 source 后
|
|
6
|
+
// 需要重新调用 ensureTmuxIntegration() 才会把 .tmux.conf 里的路径更新到位
|
|
7
|
+
// (见 docs/2026-07-15-npm-global-install-plan.md 的残留风险第 4 条)。
|
|
2
8
|
import { execa } from 'execa';
|
|
3
|
-
import { existsSync, readFileSync,
|
|
9
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
10
|
import { join } from 'node:path';
|
|
5
11
|
import { homedir } from 'node:os';
|
|
6
|
-
import { checkTmuxConf } from './symlink.js';
|
|
7
|
-
|
|
8
|
-
const PLUGIN_DECL = "set -g @plugin 'tmux-ai-hooks-status'";
|
|
12
|
+
import { checkTmuxConf, runShellDeclarationLine } from './symlink.js';
|
|
13
|
+
import { TMUX_ENTRY } from './adapters-meta.js';
|
|
9
14
|
|
|
10
15
|
function confPath() {
|
|
11
16
|
return join(homedir(), '.tmux.conf');
|
|
12
17
|
}
|
|
13
18
|
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
// 匹配「本工具此前写入的」run-shell 声明行(任意路径),用于识别指向旧 source
|
|
20
|
+
// 的陈旧行以便原地替换,避免同时存在两条 run-shell 造成 hooks/按键绑定重复注册。
|
|
21
|
+
// 只识别本工具自己的 run-shell 格式,不触碰用户可能保留的旧式 TPM @plugin 行。
|
|
22
|
+
const OWN_RUN_SHELL_RE = /^\s*run-shell\s+['"].*tmux-ai-hooks-status\.tmux['"]\s*$/;
|
|
23
|
+
|
|
24
|
+
// 确保 .tmux.conf 存在一行指向"当前 source"tmux 入口的 run-shell 声明:
|
|
25
|
+
// - 完全没有集成 → 追加新行(只追加,不改动既有内容)
|
|
26
|
+
// - 已存在本工具写入的 run-shell 行但指向其他路径(例如切换 source 之前的安装)
|
|
27
|
+
// → 原地替换为当前正确路径
|
|
28
|
+
// - 已指向当前路径 → no-op
|
|
29
|
+
// 不会删除或改动用户可能保留的旧式 TPM `@plugin` 声明行。
|
|
30
|
+
export function ensureTmuxIntegration({ tmuxEntry = TMUX_ENTRY, path = confPath() } = {}) {
|
|
31
|
+
const targetLine = runShellDeclarationLine(tmuxEntry);
|
|
32
|
+
|
|
33
|
+
if (!existsSync(path)) {
|
|
34
|
+
try {
|
|
35
|
+
writeFileSync(path, `${targetLine}\n`);
|
|
36
|
+
return { ok: true, changed: true, action: 'created', path };
|
|
37
|
+
} catch (err) {
|
|
38
|
+
return { ok: false, changed: false, path, output: String(err) };
|
|
25
39
|
}
|
|
26
|
-
|
|
27
|
-
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let raw;
|
|
43
|
+
try {
|
|
44
|
+
raw = readFileSync(path, 'utf8');
|
|
45
|
+
} catch (err) {
|
|
46
|
+
return { ok: false, changed: false, path, output: String(err) };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (raw.includes(targetLine)) {
|
|
50
|
+
return { ok: true, changed: false, action: 'noop', path };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const hadTrailingNewline = raw.endsWith('\n');
|
|
54
|
+
const lines = raw.split('\n');
|
|
55
|
+
const hasStaleOwnLine = lines.some((line) => OWN_RUN_SHELL_RE.test(line));
|
|
56
|
+
|
|
57
|
+
let nextLines;
|
|
58
|
+
if (hasStaleOwnLine) {
|
|
59
|
+
// 原地替换本工具此前写入、指向旧路径的 run-shell 行
|
|
60
|
+
nextLines = lines.map((line) => (OWN_RUN_SHELL_RE.test(line) ? targetLine : line));
|
|
61
|
+
} else {
|
|
62
|
+
// 无本工具的旧声明(可能完全未集成,或只有旧式 @plugin 行)→ 纯追加
|
|
63
|
+
nextLines = hadTrailingNewline ? [...lines.slice(0, -1), targetLine, ''] : [...lines, targetLine];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let content = nextLines.join('\n');
|
|
67
|
+
if (!content.endsWith('\n')) content += '\n';
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
writeFileSync(path, content);
|
|
71
|
+
return { ok: true, changed: true, action: hasStaleOwnLine ? 'replaced' : 'appended', path };
|
|
28
72
|
} catch (err) {
|
|
29
|
-
return { ok: false,
|
|
73
|
+
return { ok: false, changed: false, path, output: String(err) };
|
|
30
74
|
}
|
|
31
75
|
}
|
|
32
76
|
|
|
33
77
|
// 重载 tmux 配置(source-file 重跑插件加载;refresh-client 仅重绘不够)。
|
|
34
78
|
// tmux 未运行时捕获错误返回提示,不抛。
|
|
35
|
-
export async function reloadTmux() {
|
|
36
|
-
const path = confPath();
|
|
79
|
+
export async function reloadTmux({ path = confPath() } = {}) {
|
|
37
80
|
try {
|
|
38
81
|
await execa('tmux', ['source-file', path], { timeout: 10000 });
|
|
39
82
|
return { ok: true };
|
|
@@ -41,3 +84,5 @@ export async function reloadTmux() {
|
|
|
41
84
|
return { ok: false, output: err.stderr || err.shortMessage || String(err) };
|
|
42
85
|
}
|
|
43
86
|
}
|
|
87
|
+
|
|
88
|
+
export { checkTmuxConf };
|
package/src/update.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { execa } from 'execa';
|
|
3
|
+
|
|
4
|
+
const PACKAGE_NAME = 'tmuxclihook';
|
|
5
|
+
const packageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
6
|
+
|
|
7
|
+
export const CURRENT_VERSION = packageJson.version;
|
|
8
|
+
|
|
9
|
+
export async function updatePackage(options = {}) {
|
|
10
|
+
const run = options.run || execa;
|
|
11
|
+
const log = options.log || console;
|
|
12
|
+
const currentVersion = options.currentVersion || CURRENT_VERSION;
|
|
13
|
+
|
|
14
|
+
log.log(`Current version: v${currentVersion}`);
|
|
15
|
+
|
|
16
|
+
let latestVersion;
|
|
17
|
+
try {
|
|
18
|
+
const result = await run('npm', ['view', PACKAGE_NAME, 'version']);
|
|
19
|
+
latestVersion = result.stdout.trim();
|
|
20
|
+
} catch {
|
|
21
|
+
log.error('Failed to check latest version. Please check your network connection.');
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (currentVersion === latestVersion) {
|
|
26
|
+
log.log('Already up to date.');
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
log.log(`Updating to v${latestVersion}...`);
|
|
31
|
+
try {
|
|
32
|
+
await run('npm', ['install', '-g', `${PACKAGE_NAME}@latest`], { stdio: 'inherit' });
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
log.log(`Done. Updated to v${latestVersion}`);
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tmux-ai-hooks-status: AI CLI (Claude Code / Codex / opencode) hooks status for tmux
|
|
3
|
+
# TPM entry point
|
|
4
|
+
|
|
5
|
+
set -o errexit
|
|
6
|
+
set -o pipefail
|
|
7
|
+
|
|
8
|
+
CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
9
|
+
|
|
10
|
+
# --- 可自定义选项 ---
|
|
11
|
+
# 注:选项键保留 @claude_hooks_* 前缀(向后兼容用户已有 .tmux.conf 配置),
|
|
12
|
+
# 现同时作用于 claude 与 codex 状态显示。
|
|
13
|
+
STATUS_COLOR=$(tmux show-option -gv @claude_hooks_status_color 2>/dev/null || echo "#F1FA8C")
|
|
14
|
+
IDLE_ICON=$(tmux show-option -gv @claude_hooks_idle_icon 2>/dev/null || echo "✓")
|
|
15
|
+
BUSY_ICON=$(tmux show-option -gv @claude_hooks_busy_icon 2>/dev/null || echo "⠿")
|
|
16
|
+
AUTH_ICON=$(tmux show-option -gv @claude_hooks_auth_icon 2>/dev/null || echo "🔒")
|
|
17
|
+
|
|
18
|
+
# --- 多行状态栏:动态追加 AI 状态行 ---
|
|
19
|
+
# 读取当前行数(其他插件已设置好的),追加到最后一行的下一行
|
|
20
|
+
# 幂等检测:扫描所有行,查找已含 @ai_all_status 签名的行,直接复用,
|
|
21
|
+
# 不再追加——这样手动重载脚本不会累计增加行数
|
|
22
|
+
_cur_status=$(tmux show-option -gv status 2>/dev/null || echo "on")
|
|
23
|
+
case "$_cur_status" in
|
|
24
|
+
on) _cur_rows=1 ;;
|
|
25
|
+
off) _cur_rows=0 ;;
|
|
26
|
+
[0-9]*) _cur_rows=$((_cur_status + 0)) ;;
|
|
27
|
+
*) _cur_rows=1 ;;
|
|
28
|
+
esac
|
|
29
|
+
CLAUDE_ROW=""
|
|
30
|
+
for _i in $(seq 0 $((_cur_rows - 1))); do
|
|
31
|
+
_fmt=$(tmux show-option -gv "status-format[${_i}]" 2>/dev/null || true)
|
|
32
|
+
if echo "$_fmt" | grep -q "@ai_all_status"; then
|
|
33
|
+
CLAUDE_ROW=$_i
|
|
34
|
+
break
|
|
35
|
+
fi
|
|
36
|
+
done
|
|
37
|
+
if [ -z "$CLAUDE_ROW" ]; then
|
|
38
|
+
CLAUDE_ROW=$_cur_rows
|
|
39
|
+
tmux set-option -g status $((_cur_rows + 1)) 2>/dev/null || true
|
|
40
|
+
fi
|
|
41
|
+
tmux set-option -g "status-format[${CLAUDE_ROW}]" "#[align=centre]#{?#{@ai_all_status},#{T:@ai_all_status},}" 2>/dev/null || true
|
|
42
|
+
|
|
43
|
+
# 不动 status-right,让用户自行管理第 1 行内容
|
|
44
|
+
# Claude 状态通过多行 status-format 的独立行显示,不修改 status-right
|
|
45
|
+
|
|
46
|
+
# --- Reload 保护:覆盖 prefix+r reload 绑定,source-file 后追加本插件初始化 ---
|
|
47
|
+
# 用哨兵变量防止重复覆盖(tmux server 生命周期内只覆盖一次)
|
|
48
|
+
# 注意:不能在 shell CLI 中用 \; 分隔多条 tmux 命令传给 bind-key,
|
|
49
|
+
# 因为 tmux CLI 会将 \; 作为命令分隔符立即执行后续命令(而不是绑定到按键)。
|
|
50
|
+
# 解决方案:写入临时 tmux config 文件再 source,确保 \; 被 tmux config parser 正确处理。
|
|
51
|
+
if [ -z "$(tmux show-option -gv @claude_hooks_reload_registered 2>/dev/null)" ]; then
|
|
52
|
+
_bind_tmpfile=$(mktemp /tmp/tmux-ai-bind-XXXXXX.conf)
|
|
53
|
+
printf "bind-key r source-file ~/.tmux.conf \\; run-shell '%s' \\; display-message '配置已重载'\n" \
|
|
54
|
+
"${CURRENT_DIR}/tmux-ai-hooks-status.tmux" > "$_bind_tmpfile"
|
|
55
|
+
tmux source-file "$_bind_tmpfile" 2>/dev/null || true
|
|
56
|
+
rm -f "$_bind_tmpfile"
|
|
57
|
+
tmux set-option -g @claude_hooks_reload_registered "1"
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# --- 自动注册 Claude hooks(幂等,每次插件加载时确保 hooks 存在)---
|
|
61
|
+
# 注:codex/opencode hooks 不自动安装(非人人安装),用 prefix+M-h / prefix+C-p 手动装。
|
|
62
|
+
"${CURRENT_DIR}/scripts/install-claude-hooks.sh" >/dev/null 2>&1 || true
|
|
63
|
+
|
|
64
|
+
# --- 启动 tmux server 级状态监控器 ---
|
|
65
|
+
# Codex/opencode 均无 SessionEnd(codex 的 SessionStart 还延迟到首个 turn);
|
|
66
|
+
# monitor 负责检测前台工具启动和退出。每次 reload 发布新 generation,旧 monitor
|
|
67
|
+
# 自动退出,始终只保留一个有效实例。
|
|
68
|
+
"${CURRENT_DIR}/scripts/tmux-ai-monitor" start >/dev/null 2>&1 || true
|
|
69
|
+
|
|
70
|
+
# --- 注册 tmux session/client 变化 hook,刷新聚合状态 ---
|
|
71
|
+
# session-closed: session 被销毁时清除残留条目
|
|
72
|
+
# client-detached: client 断开连接时排除已变为 detached 的 session
|
|
73
|
+
# client-attached: client 重新连接时恢复该 session 的状态
|
|
74
|
+
# 注:_refresh 与 TOOL_ID 无关(仅重建聚合 + 自修复 claude hooks),统一传 claude。
|
|
75
|
+
tmux set-hook -g session-closed "run-shell '${CURRENT_DIR}/scripts/tmux-ai-status claude _refresh'"
|
|
76
|
+
tmux set-hook -g client-detached "run-shell '${CURRENT_DIR}/scripts/tmux-ai-status claude _refresh'"
|
|
77
|
+
tmux set-hook -g client-attached "run-shell '${CURRENT_DIR}/scripts/tmux-ai-status claude _refresh'"
|
|
78
|
+
# pane-exited: 程序自然退出 → 触发孤儿清理(#{pane_id} 在此 hook 中不可靠)
|
|
79
|
+
tmux set-hook -g pane-exited "run-shell '${CURRENT_DIR}/scripts/tmux-ai-status claude _refresh'"
|
|
80
|
+
# after-kill-pane: kill-pane 后触发孤儿清理
|
|
81
|
+
tmux set-hook -g after-kill-pane "run-shell '${CURRENT_DIR}/scripts/tmux-ai-status claude _refresh'"
|
|
82
|
+
# pane-focus-in: 切换到 pane 时完整刷新(清理 + 重建聚合)。
|
|
83
|
+
# Codex 的持续刷新由 tmux-ai-monitor 负责;focus hook 仍作为立即刷新路径,避免等待
|
|
84
|
+
# 下一个 1s tick。空闲无显式状态时 build_all_status 会按前台 codex 进程补 idle `-`。
|
|
85
|
+
tmux set-hook -g pane-focus-in "run-shell '${CURRENT_DIR}/scripts/tmux-ai-status claude _refresh'"
|
|
86
|
+
|
|
87
|
+
# --- 快捷键绑定 ---
|
|
88
|
+
# prefix + C-h: 安装 Claude hooks 到 ~/.claude/settings.json
|
|
89
|
+
tmux bind-key C-h run-shell "${CURRENT_DIR}/scripts/install-claude-hooks.sh && tmux display 'Claude hooks installed'"
|
|
90
|
+
# prefix + C-u: 卸载 Claude hooks
|
|
91
|
+
tmux bind-key C-u run-shell "${CURRENT_DIR}/scripts/install-claude-hooks.sh uninstall && tmux display 'Claude hooks removed'"
|
|
92
|
+
# prefix + M-h: 安装 Codex hooks 到 ~/.codex/hooks.json(需 codex ≥ v0.144)
|
|
93
|
+
tmux bind-key M-h run-shell "${CURRENT_DIR}/scripts/install-codex-hooks.sh && tmux display 'Codex hooks installed'"
|
|
94
|
+
# prefix + M-u: 卸载 Codex hooks
|
|
95
|
+
tmux bind-key M-u run-shell "${CURRENT_DIR}/scripts/install-codex-hooks.sh uninstall && tmux display 'Codex hooks removed'"
|
|
96
|
+
# prefix + C-p: 安装 opencode plugin 到 ~/.config/opencode/plugins/
|
|
97
|
+
tmux bind-key C-p run-shell "${CURRENT_DIR}/scripts/install-opencode-hooks.sh && tmux display 'opencode plugin installed'"
|
|
98
|
+
# prefix + M-p: 卸载 opencode plugin
|
|
99
|
+
tmux bind-key M-p run-shell "${CURRENT_DIR}/scripts/install-opencode-hooks.sh uninstall && tmux display 'opencode plugin removed'"
|
|
100
|
+
# prefix + I: 打开交互式 TUI 安装器(环境检查/侦测CLI/装卸修hooks/tmux集成/source管理)
|
|
101
|
+
# 用 new-window 以获得真实终端(TUI 需交互,不能走后台 run-shell)。
|
|
102
|
+
# 优先用全局命令 tmuxclihook(npm i -g 后可用,跟随当前 source 自动解析路径);
|
|
103
|
+
# 找不到则回落本地开发布局(installer/ 子目录存在则用之,否则用 CURRENT_DIR 自身,
|
|
104
|
+
# 覆盖"scripts/ 与 tmux 入口已被 npm 打包进同一目录"的场景)。
|
|
105
|
+
if command -v tmuxclihook >/dev/null 2>&1; then
|
|
106
|
+
tmux bind-key I new-window "tmuxclihook"
|
|
107
|
+
elif [ -d "${CURRENT_DIR}/installer" ]; then
|
|
108
|
+
tmux bind-key I new-window -c "${CURRENT_DIR}/installer" "node bin/cli.js"
|
|
109
|
+
else
|
|
110
|
+
tmux bind-key I new-window -c "${CURRENT_DIR}" "node bin/cli.js"
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
# Esc 拒绝检测:! / ? 状态下 Esc → -
|
|
114
|
+
tmux bind-key -n Escape run-shell "${CURRENT_DIR}/scripts/tmux-ai-esc"
|