terminal-bridge-setup 2.0.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.
@@ -0,0 +1,216 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <style>
6
+ body {
7
+ width: 340px;
8
+ padding: 12px;
9
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
10
+ font-size: 13px;
11
+ margin: 0;
12
+ }
13
+ h3 { margin: 0 0 4px; font-size: 15px; }
14
+ .subtitle { font-size: 11px; color: #888; margin-bottom: 8px; }
15
+ button {
16
+ width: 100%;
17
+ padding: 9px;
18
+ margin: 4px 0;
19
+ cursor: pointer;
20
+ border: none;
21
+ border-radius: 5px;
22
+ font-size: 13px;
23
+ font-weight: 500;
24
+ transition: opacity .15s;
25
+ }
26
+ button:hover { opacity: .9; }
27
+ .proxy-start { background: #FF9800; color: white; }
28
+ .proxy-stop { background: #795548; color: white; }
29
+ .scan { background: #2196F3; color: white; }
30
+ .status-line {
31
+ display: flex;
32
+ align-items: center;
33
+ font-size: 12px;
34
+ margin-bottom: 6px;
35
+ }
36
+ .dot {
37
+ display: inline-block;
38
+ width: 8px;
39
+ height: 8px;
40
+ border-radius: 50%;
41
+ margin-right: 6px;
42
+ flex-shrink: 0;
43
+ }
44
+ .dot.on { background: #4CAF50; }
45
+ .dot.off { background: #ccc; }
46
+ .dot.loading { background: #FF9800; animation: pulse 1s infinite; }
47
+ @keyframes pulse { 50% { opacity: 0.4; } }
48
+ .section {
49
+ margin-top: 12px;
50
+ padding-top: 10px;
51
+ border-top: 1px solid #e0e0e0;
52
+ }
53
+ .section-title {
54
+ font-size: 11px;
55
+ color: #888;
56
+ text-transform: uppercase;
57
+ letter-spacing: 0.5px;
58
+ margin-bottom: 8px;
59
+ }
60
+ .hint {
61
+ font-size: 11px;
62
+ color: #888;
63
+ margin-top: 4px;
64
+ line-height: 1.5;
65
+ }
66
+ .tab-item {
67
+ display: flex;
68
+ align-items: center;
69
+ padding: 6px 8px;
70
+ margin: 3px 0;
71
+ border-radius: 4px;
72
+ background: #f5f5f5;
73
+ font-size: 12px;
74
+ cursor: pointer;
75
+ transition: background .15s;
76
+ }
77
+ .tab-item:hover { background: #e8e8e8; }
78
+ .tab-item.active {
79
+ background: #e3f2fd;
80
+ border-left: 3px solid #2196F3;
81
+ }
82
+ .tab-item .tab-title {
83
+ flex: 1;
84
+ overflow: hidden;
85
+ text-overflow: ellipsis;
86
+ white-space: nowrap;
87
+ margin-left: 6px;
88
+ }
89
+ .tab-item .tab-check { font-size: 14px; flex-shrink: 0; }
90
+ .tab-item .tab-host {
91
+ font-size: 10px;
92
+ color: #888;
93
+ background: #e8e8e8;
94
+ padding: 1px 5px;
95
+ border-radius: 3px;
96
+ margin-left: 6px;
97
+ flex-shrink: 0;
98
+ max-width: 90px;
99
+ overflow: hidden;
100
+ text-overflow: ellipsis;
101
+ white-space: nowrap;
102
+ }
103
+ .tab-item.active .tab-host { background: #bbdefb; color: #1565c0; }
104
+ .tab-item .tab-current {
105
+ font-size: 9px;
106
+ color: #4CAF50;
107
+ margin-left: 4px;
108
+ flex-shrink: 0;
109
+ }
110
+ .tab-item .tab-info { display: flex; flex-direction: column; flex: 1; min-width: 0; }
111
+ .tab-item .tab-title {
112
+ overflow: hidden;
113
+ text-overflow: ellipsis;
114
+ white-space: nowrap;
115
+ margin-left: 6px;
116
+ }
117
+ .tab-item .tab-row { display: flex; align-items: center; margin-left: 6px; flex: 1; min-width: 0; }
118
+ .guide {
119
+ font-size: 11px;
120
+ color: #666;
121
+ line-height: 1.7;
122
+ }
123
+ .guide code {
124
+ background: #f0f0f0;
125
+ padding: 1px 4px;
126
+ border-radius: 3px;
127
+ font-size: 10px;
128
+ }
129
+ .guide ol { margin: 4px 0; padding-left: 18px; }
130
+ .install-hint {
131
+ margin-top: 8px;
132
+ padding: 8px;
133
+ background: #fff3e0;
134
+ border: 1px solid #ffb74d;
135
+ border-radius: 4px;
136
+ font-size: 11px;
137
+ color: #e65100;
138
+ }
139
+ .install-hint .cmd-row {
140
+ display: flex;
141
+ align-items: center;
142
+ gap: 6px;
143
+ margin: 6px 0 4px;
144
+ }
145
+ .install-hint code {
146
+ flex: 1;
147
+ background: #fff;
148
+ padding: 4px 6px;
149
+ border-radius: 3px;
150
+ font-size: 10px;
151
+ color: #333;
152
+ font-family: "SF Mono", Menlo, monospace;
153
+ }
154
+ .copy-btn {
155
+ width: auto;
156
+ padding: 4px 10px;
157
+ margin: 0;
158
+ font-size: 10px;
159
+ background: #ff9800;
160
+ color: white;
161
+ border-radius: 3px;
162
+ flex-shrink: 0;
163
+ }
164
+ </style>
165
+ </head>
166
+ <body>
167
+ <h3>🔌 终端桥接</h3>
168
+ <div class="subtitle">JumpServer · Arthas Web Console</div>
169
+
170
+ <div class="section" style="margin-top:8px;padding-top:0;border:none;">
171
+ <div class="status-line">
172
+ <span class="dot off" id="xtermDot"></span>
173
+ <span id="xtermStatusText">未检测到终端</span>
174
+ </div>
175
+ <div class="hint" id="xtermHint">打开 JumpServer 终端 或 Arthas Console</div>
176
+ <button class="scan" id="btnXtermScan">🔍 捕捉终端</button>
177
+ <div id="tabList" style="margin-top:6px;"></div>
178
+ </div>
179
+
180
+ <div class="section">
181
+ <div class="section-title">桥接代理</div>
182
+ <div class="status-line">
183
+ <span class="dot off" id="proxyDot"></span>
184
+ <span id="proxyStatusText">代理未运行</span>
185
+ </div>
186
+ <button class="proxy-start" id="btnProxyStart">🚀 启动代理</button>
187
+ <button class="proxy-stop" id="btnProxyStop" style="display:none;">⏹ 停止代理</button>
188
+ <div id="proxyInstallHint" class="install-hint" style="display:none;">
189
+ <div>⚠ 未安装本地宿主,代理无法启动</div>
190
+ <div class="cmd-row">
191
+ <code id="installCmd">npx terminal-bridge-setup</code>
192
+ <button class="copy-btn" id="btnCopyInstall">复制</button>
193
+ </div>
194
+ <div class="hint">在终端运行此命令完成安装,然后重新加载插件</div>
195
+ </div>
196
+ </div>
197
+
198
+ <div class="section">
199
+ <div class="section-title">使用说明</div>
200
+ <div class="guide">
201
+ <ol>
202
+ <li>打开终端页面(JumpServer Web 终端 / Arthas Console),完成连接</li>
203
+ <li>点「🔍 捕捉终端」(灰点变绿)。多开时在下方选目标 tab</li>
204
+ <li>点「🚀 启动代理」(橙点变绿)</li>
205
+ <li>两个都绿,Agent 即可通过桥接发命令</li>
206
+ </ol>
207
+ <div class="hint" style="margin-top:6px;">
208
+ ⚠ 交互式命令(vim/less/需密码的 sudo)会超时<br>
209
+ ⚠ Arthas 高风险命令(retransform/profiler/stop/reset)已禁用
210
+ </div>
211
+ </div>
212
+ </div>
213
+
214
+ <script src="popup.js"></script>
215
+ </body>
216
+ </html>
@@ -0,0 +1,185 @@
1
+ // JumpServer 终端桥接 — popup 交互
2
+ // 只管两件事:xterm 捕获/选择 + 代理启停
3
+
4
+ // ============== xterm 终端状态 ==============
5
+ const xtermDot = document.getElementById('xtermDot');
6
+ const xtermText = document.getElementById('xtermStatusText');
7
+ const xtermHint = document.getElementById('xtermHint');
8
+ const tabListEl = document.getElementById('tabList');
9
+
10
+ function refreshXterm() {
11
+ chrome.runtime.sendMessage({ type: 'XTERM_STATUS' }, (res) => {
12
+ if (chrome.runtime.lastError || !res) {
13
+ xtermDot.className = 'dot off';
14
+ xtermText.textContent = '状态未知';
15
+ tabListEl.innerHTML = '';
16
+ return;
17
+ }
18
+ if (res.ready) {
19
+ xtermDot.className = 'dot on';
20
+ xtermText.textContent = `xterm 已捕获 (${res.tabCount} 个终端)`;
21
+ xtermHint.textContent = res.attachedCount > 0
22
+ ? `✓ 已 attach,可执行命令`
23
+ : `⚠ 等待 attach...`;
24
+ // 渲染 tab 列表
25
+ if (res.tabs && res.tabs.length > 0) {
26
+ tabListEl.innerHTML = res.tabs.map(t => {
27
+ const hostTag = t.host ? `<span class="tab-host" title="${t.host}">${t.host}</span>` : '';
28
+ const currentTag = t.isCurrent ? '<span class="tab-current">● 当前</span>' : '';
29
+ return `<div class="tab-item ${t.active ? 'active' : ''}" data-tabid="${t.tabId}">
30
+ <span class="tab-check">${t.active ? '◉' : '○'}</span>
31
+ <span class="tab-row">
32
+ <span class="tab-title" title="${t.title}">${t.title}</span>
33
+ ${hostTag}${currentTag}
34
+ </span>
35
+ </div>`;
36
+ }).join('');
37
+ tabListEl.querySelectorAll('.tab-item').forEach(el => {
38
+ el.onclick = () => {
39
+ const tabId = parseInt(el.dataset.tabid, 10);
40
+ chrome.runtime.sendMessage({ type: 'XTERM_SELECT', tabId }, () => refreshXterm());
41
+ };
42
+ });
43
+ } else {
44
+ tabListEl.innerHTML = '';
45
+ }
46
+ } else {
47
+ xtermDot.className = 'dot off';
48
+ xtermText.textContent = '未检测到终端';
49
+ xtermHint.textContent = '打开 JumpServer 终端 或 Arthas Console';
50
+ tabListEl.innerHTML = '';
51
+ }
52
+ });
53
+ }
54
+
55
+ document.getElementById('btnXtermScan').onclick = () => {
56
+ xtermDot.className = 'dot loading';
57
+ xtermText.textContent = '扫描中...';
58
+ xtermHint.textContent = '正在遍历所有 tab 查找 xterm';
59
+ chrome.runtime.sendMessage({ type: 'XTERM_SCAN' }, (res) => {
60
+ if (chrome.runtime.lastError || !res) {
61
+ xtermDot.className = 'dot off';
62
+ xtermText.textContent = '扫描失败';
63
+ return;
64
+ }
65
+ if (res.found > 0) {
66
+ xtermDot.className = 'dot on';
67
+ xtermText.textContent = `✓ 捕获到 ${res.found} 个终端`;
68
+ xtermHint.textContent = '已自动 attach,可以执行命令';
69
+ setTimeout(refreshXterm, 800);
70
+ } else {
71
+ xtermDot.className = 'dot off';
72
+ xtermText.textContent = '未找到终端';
73
+ xtermHint.textContent = '请确认终端页面已打开并完成连接';
74
+ }
75
+ });
76
+ };
77
+
78
+ // ============== 代理控制 ==============
79
+ const proxyDot = document.getElementById('proxyDot');
80
+ const proxyText = document.getElementById('proxyStatusText');
81
+ const btnProxyStart = document.getElementById('btnProxyStart');
82
+ const btnProxyStop = document.getElementById('btnProxyStop');
83
+ const proxyInstallHint = document.getElementById('proxyInstallHint');
84
+
85
+ // 判断是不是 native host 未安装(连接被拒/超时/无响应)
86
+ function isNativeHostMissing(res) {
87
+ if (!res) return true; // 完全无响应
88
+ if (res.ok === false) {
89
+ const msg = (res.msg || '').toLowerCase();
90
+ return msg.includes('native host') || msg.includes('连接失败') ||
91
+ msg.includes('未安装') || msg.includes('无法连接') ||
92
+ msg.includes('access forbidden');
93
+ }
94
+ return false;
95
+ }
96
+
97
+ function showInstallHint(show) {
98
+ proxyInstallHint.style.display = show ? 'block' : 'none';
99
+ }
100
+
101
+ function setProxyUI(state, text) {
102
+ proxyDot.className = 'dot ' + state;
103
+ proxyText.textContent = text;
104
+ if (state === 'on') {
105
+ btnProxyStart.style.display = 'none';
106
+ btnProxyStop.style.display = 'block';
107
+ } else if (state === 'off') {
108
+ btnProxyStart.style.display = 'block';
109
+ btnProxyStop.style.display = 'none';
110
+ }
111
+ }
112
+
113
+ function refreshProxy() {
114
+ // 注意:不要在查询前置 loading,否则定时刷新每 3 秒会闪一次黄。
115
+ // 保持上次的 UI 状态不动,拿到结果后才更新。
116
+ chrome.runtime.sendMessage({ type: 'PROXY_STATUS' }, (res) => {
117
+ if (chrome.runtime.lastError || !res) {
118
+ setProxyUI('off', '状态未知');
119
+ return;
120
+ }
121
+ if (res.ok && res.status === 'running') {
122
+ setProxyUI('on', `运行中 · 127.0.0.1:${res.port || 8787}`);
123
+ } else if (res.ok) {
124
+ setProxyUI('off', '代理未运行');
125
+ } else {
126
+ setProxyUI('off', res.msg || '代理未运行');
127
+ }
128
+ });
129
+ }
130
+
131
+ btnProxyStart.onclick = () => {
132
+ setProxyUI('loading', '启动中...');
133
+ showInstallHint(false);
134
+ chrome.runtime.sendMessage({ type: 'PROXY_START' }, (res) => {
135
+ if (chrome.runtime.lastError || !res) {
136
+ setProxyUI('off', '启动失败(无响应)');
137
+ showInstallHint(true); // 无响应多半是 native host 没装
138
+ return;
139
+ }
140
+ if (res.ok && res.status === 'running') {
141
+ setProxyUI('on', `✓ 运行中 · 127.0.0.1:${res.port || 8787}`);
142
+ } else {
143
+ setProxyUI('off', '✗ ' + (res.msg || '启动失败'));
144
+ // native host 未安装时显示安装命令
145
+ if (isNativeHostMissing(res)) showInstallHint(true);
146
+ }
147
+ });
148
+ };
149
+
150
+ btnProxyStop.onclick = () => {
151
+ setProxyUI('loading', '停止中...');
152
+ chrome.runtime.sendMessage({ type: 'PROXY_STOP' }, (res) => {
153
+ if (chrome.runtime.lastError || !res) {
154
+ setProxyUI('off', '已停止');
155
+ return;
156
+ }
157
+ setProxyUI('off', res.msg || '代理已停止');
158
+ });
159
+ };
160
+
161
+ // 复制安装命令
162
+ document.getElementById('btnCopyInstall').onclick = () => {
163
+ const cmd = document.getElementById('installCmd').textContent;
164
+ navigator.clipboard.writeText(cmd).then(() => {
165
+ const btn = document.getElementById('btnCopyInstall');
166
+ const orig = btn.textContent;
167
+ btn.textContent = '✓ 已复制';
168
+ setTimeout(() => { btn.textContent = orig; }, 1500);
169
+ }).catch(() => {
170
+ // clipboard API 可能因权限失败,fallback 选中文本
171
+ const range = document.createRange();
172
+ range.selectNode(document.getElementById('installCmd'));
173
+ getSelection().removeAllRanges();
174
+ getSelection().addRange(range);
175
+ });
176
+ };
177
+
178
+ // 定时刷新(popup 打开期间)
179
+ setInterval(() => { refreshXterm(); refreshProxy(); }, 3000);
180
+ refreshXterm();
181
+ refreshProxy();
182
+ // 启动时主动探测一次 native host:发个 status,失败就显示安装提示
183
+ chrome.runtime.sendMessage({ type: 'PROXY_STATUS' }, (res) => {
184
+ if (isNativeHostMissing(res)) showInstallHint(true);
185
+ });
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "com.wssniffer.host",
3
+ "description": "Terminal bridge native messaging host",
4
+ "path": "__HOST_SH__",
5
+ "type": "stdio",
6
+ "allowed_origins": [
7
+ "chrome-extension://__EXTENSION_ID__/"
8
+ ]
9
+ }
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env node
2
+ // WS Sniffer Native Messaging Host
3
+ //
4
+ // Chrome 通过 native messaging 和这个脚本通信(stdin/stdout,4 字节小端长度前缀)。
5
+ // 职责:接收插件命令,启动/停止/查询 proxy/server.js。
6
+ //
7
+ // 关键设计:proxy 是独立的守护进程,不跟随 host 退出。
8
+ // - start: 用 detached + unref 启动 proxy,PID 写到 PID 文件,立即返回
9
+ // - stop: 读 PID 文件,发 SIGTERM
10
+ // - status: 检查端口是否在监听 + PID 文件是否存在
11
+ // host 进程本身是短命的(每次命令一个连接),proxy 持久运行。
12
+
13
+ const { spawn, execSync } = require("node:child_process");
14
+ const path = require("node:path");
15
+ const fs = require("node:fs");
16
+ const net = require("node:net");
17
+
18
+ const NODE_BIN = process.execPath;
19
+ const PROXY_DIR = path.resolve(__dirname, "..", "proxy");
20
+ const PROXY_SCRIPT = path.join(PROXY_DIR, "server.js");
21
+ const PROXY_PORT = Number(process.env.JTB_PORT || 8787);
22
+ const PID_FILE = path.join(PROXY_DIR, ".proxy.pid");
23
+ const LOG_FILE = path.join(PROXY_DIR, ".proxy.log");
24
+
25
+ // ============== Native messaging I/O ==============
26
+ function sendMessage(obj) {
27
+ const json = Buffer.from(JSON.stringify(obj), "utf8");
28
+ const header = Buffer.alloc(4);
29
+ header.writeUInt32LE(json.length, 0);
30
+ process.stdout.write(Buffer.concat([header, json]));
31
+ }
32
+
33
+ function readMessages(onMessage) {
34
+ let buf = Buffer.alloc(0);
35
+ process.stdin.on("data", (chunk) => {
36
+ buf = Buffer.concat([buf, chunk]);
37
+ while (buf.length >= 4) {
38
+ const len = buf.readUInt32LE(0);
39
+ if (buf.length < 4 + len) break;
40
+ const msg = JSON.parse(buf.slice(4, 4 + len).toString("utf8"));
41
+ buf = buf.slice(4 + len);
42
+ onMessage(msg);
43
+ }
44
+ });
45
+ process.stdin.on("end", () => process.exit(0));
46
+ }
47
+
48
+ // ============== 工具函数 ==============
49
+ function isProxyListening() {
50
+ return new Promise((resolve) => {
51
+ const sock = new net.Socket();
52
+ sock.setTimeout(800);
53
+ sock.on("connect", () => { sock.destroy(); resolve(true); });
54
+ sock.on("error", () => resolve(false));
55
+ sock.on("timeout", () => { sock.destroy(); resolve(false); });
56
+ sock.connect(PROXY_PORT, "127.0.0.1");
57
+ });
58
+ }
59
+
60
+ function readPid() {
61
+ try {
62
+ const pid = parseInt(fs.readFileSync(PID_FILE, "utf8").trim(), 10);
63
+ return isNaN(pid) ? null : pid;
64
+ } catch { return null; }
65
+ }
66
+
67
+ function writePid(pid) {
68
+ try { fs.writeFileSync(PID_FILE, String(pid)); } catch {}
69
+ }
70
+
71
+ function clearPid() {
72
+ try { fs.unlinkSync(PID_FILE); } catch {}
73
+ }
74
+
75
+ function isPidAlive(pid) {
76
+ try {
77
+ process.kill(pid, 0); // 信号 0 = 探活,不发实际信号
78
+ return true;
79
+ } catch { return false; }
80
+ }
81
+
82
+ // ============== 命令处理 ==============
83
+ async function startProxy() {
84
+ // 已在运行?
85
+ if (await isProxyListening()) {
86
+ return { ok: true, status: "running", msg: "proxy already running", port: PROXY_PORT };
87
+ }
88
+
89
+ // 清理失效的 PID 文件
90
+ const oldPid = readPid();
91
+ if (oldPid && !isPidAlive(oldPid)) clearPid();
92
+
93
+ // 打开日志文件(append 模式),proxy 的 stdout/stderr 都写进去
94
+ const logFd = fs.openSync(LOG_FILE, "a");
95
+
96
+ try {
97
+ const child = spawn(NODE_BIN, [PROXY_SCRIPT], {
98
+ cwd: PROXY_DIR,
99
+ stdio: ["ignore", logFd, logFd],
100
+ env: { ...process.env, PORT: String(PROXY_PORT) },
101
+ detached: true, // 关键:脱离父进程,变成独立进程组
102
+ });
103
+ // unref 让父进程(host)可以不等子进程就退出
104
+ child.unref();
105
+ writePid(child.pid);
106
+ fs.writeFileSync(logFd, `\n[host] proxy started PID=${child.pid} at ${new Date().toISOString()}\n`);
107
+
108
+ // 等一小段时间确认端口起来
109
+ await new Promise(r => setTimeout(r, 800));
110
+ const up = await isProxyListening();
111
+ if (up) {
112
+ return { ok: true, status: "running", msg: "proxy started", port: PROXY_PORT };
113
+ } else {
114
+ return { ok: true, status: "running", msg: "proxy process started (port not ready yet)", port: PROXY_PORT };
115
+ }
116
+ } catch (err) {
117
+ return { ok: false, status: "stopped", msg: "start error: " + err.message };
118
+ }
119
+ }
120
+
121
+ function stopProxy() {
122
+ return new Promise(async (resolve) => {
123
+ const pid = readPid();
124
+ if (!pid) {
125
+ // 没有 PID 文件,但端口可能在监听(手动启动的),尝试用 lsof 找
126
+ const up = await isProxyListening();
127
+ resolve(up
128
+ ? { ok: false, status: "running", msg: "无法停止:PID 文件缺失(可能是手动启动的代理),请手动 kill" }
129
+ : { ok: true, status: "stopped", msg: "proxy was not running" });
130
+ return;
131
+ }
132
+ try {
133
+ process.kill(pid, "SIGTERM");
134
+ // 等 2s 确认退出
135
+ setTimeout(() => {
136
+ if (isPidAlive(pid)) {
137
+ try { process.kill(pid, "SIGKILL"); } catch {}
138
+ }
139
+ clearPid();
140
+ resolve({ ok: true, status: "stopped", msg: "proxy stopped" });
141
+ }, 2000);
142
+ } catch (err) {
143
+ clearPid();
144
+ resolve({ ok: true, status: "stopped", msg: "proxy was not running (stale pid)" });
145
+ }
146
+ });
147
+ }
148
+
149
+ async function status() {
150
+ const up = await isProxyListening();
151
+ return { ok: true, status: up ? "running" : "stopped", port: PROXY_PORT };
152
+ }
153
+
154
+ // ============== 主循环 ==============
155
+ readMessages(async (msg) => {
156
+ const cmd = msg && msg.cmd;
157
+ if (cmd === "start") {
158
+ sendMessage(await startProxy());
159
+ } else if (cmd === "stop") {
160
+ sendMessage(await stopProxy());
161
+ } else if (cmd === "status") {
162
+ sendMessage(await status());
163
+ } else {
164
+ sendMessage({ ok: false, msg: "unknown cmd: " + cmd });
165
+ }
166
+ });
167
+
168
+ process.stderr.write("[host] native messaging host ready\n");
@@ -0,0 +1,5 @@
1
+ #!/bin/bash
2
+ # Native messaging host 启动 wrapper
3
+ # 由安装脚本(install.sh / npx 包)生成,__NODE_BIN__ 和 __HOST_JS__ 会被替换成实际路径
4
+ # 用绝对路径调 node,避免 Chrome 启动时 PATH 不全找不到 node
5
+ exec __NODE_BIN__ __HOST_JS__
@@ -0,0 +1,89 @@
1
+ #!/bin/bash
2
+ # 安装 Native Messaging Host(通用版,不写死路径)
3
+ #
4
+ # 这个脚本被两种方式调用:
5
+ # 1. 直接运行:bash native/install.sh(开发模式,用当前项目目录)
6
+ # 2. 被 npx 安装包调用:传入 INSTALL_DIR 参数(释放目录,如 ~/.terminal-bridge)
7
+ #
8
+ # 做的事:
9
+ # 1. 检测 node 可执行文件路径
10
+ # 2. 从模板生成 host.sh(填入实际 node 路径 + host.js 路径)
11
+ # 3. 从模板生成 com.wssniffer.host.json(填入实际 host.sh 路径 + 插件 ID)
12
+ # 4. 复制 manifest 到 Chrome 的 NativeMessagingHosts 目录
13
+ # 5. 安装 proxy 的 npm 依赖(ws)
14
+ #
15
+ # 卸载:rm "$HOME/Library/Application Support/Google/Chrome/NativeMessagingHosts/com.wssniffer.host.json"
16
+
17
+ set -e
18
+
19
+ # ---------- 路径解析 ----------
20
+ # SCRIPT_DIR: 本脚本所在目录(native/)
21
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
22
+
23
+ # INSTALL_DIR: 项目根目录(native 的上一级)
24
+ # 支持外部传入(npx 包用),否则用相对路径推断
25
+ INSTALL_DIR="${1:-$(cd "$SCRIPT_DIR/.." && pwd)}"
26
+
27
+ NATIVE_DIR="$INSTALL_DIR/native"
28
+ PROXY_DIR="$INSTALL_DIR/proxy"
29
+
30
+ # ---------- 配置 ----------
31
+ # 插件 ID(由 manifest.json 的 key 字段决定,固定不变)
32
+ EXTENSION_ID="jkbnakjnbahigfefgiipfngheiafoein"
33
+
34
+ # Chrome native messaging host 目录(macOS)
35
+ DEST_DIR="$HOME/Library/Application Support/Google/Chrome/NativeMessagingHosts"
36
+ DEST="$DEST_DIR/com.wssniffer.host.json"
37
+
38
+ # ---------- 前置检查 ----------
39
+ echo "=== 安装 Terminal Bridge Native Host ==="
40
+ echo " 安装目录: $INSTALL_DIR"
41
+
42
+ # 检测 node
43
+ if ! command -v node > /dev/null 2>&1; then
44
+ echo "✗ 未找到 node,请先安装 Node.js (https://nodejs.org)"
45
+ exit 1
46
+ fi
47
+ NODE_BIN="$(command -v node)"
48
+ echo " node 路径: $NODE_BIN ($(node --version))"
49
+
50
+ # 检查模板存在
51
+ if [ ! -f "$NATIVE_DIR/host.sh.template" ]; then
52
+ echo "✗ 未找到 $NATIVE_DIR/host.sh.template,项目结构不完整"
53
+ exit 1
54
+ fi
55
+
56
+ # ---------- 1. 生成 host.sh ----------
57
+ HOST_SH="$NATIVE_DIR/host.sh"
58
+ HOST_JS="$NATIVE_DIR/host.js"
59
+ sed -e "s|__NODE_BIN__|$NODE_BIN|g" \
60
+ -e "s|__HOST_JS__|$HOST_JS|g" \
61
+ "$NATIVE_DIR/host.sh.template" > "$HOST_SH"
62
+ chmod +x "$HOST_SH"
63
+ echo "✓ 生成 host.sh → $HOST_SH"
64
+
65
+ # ---------- 2. 生成 com.wssniffer.host.json ----------
66
+ TMP_MANIFEST="$NATIVE_DIR/com.wssniffer.host.json"
67
+ sed -e "s|__HOST_SH__|$HOST_SH|g" \
68
+ -e "s|__EXTENSION_ID__|$EXTENSION_ID|g" \
69
+ "$NATIVE_DIR/com.wssniffer.host.json.template" > "$TMP_MANIFEST"
70
+ echo "✓ 生成 manifest → $TMP_MANIFEST"
71
+
72
+ # ---------- 3. 注册到 Chrome ----------
73
+ mkdir -p "$DEST_DIR"
74
+ cp "$TMP_MANIFEST" "$DEST"
75
+ echo "✓ 已注册到 Chrome: $DEST"
76
+
77
+ # ---------- 4. 安装 proxy 依赖 ----------
78
+ if [ -f "$PROXY_DIR/package.json" ]; then
79
+ if [ ! -d "$PROXY_DIR/node_modules/ws" ]; then
80
+ echo "→ 安装 proxy 依赖 (ws)..."
81
+ (cd "$PROXY_DIR" && npm install --silent)
82
+ echo "✓ 依赖已安装"
83
+ else
84
+ echo "✓ proxy 依赖已存在"
85
+ fi
86
+ fi
87
+
88
+ echo ""
89
+ echo "=== Native Host 安装完成 ==="