@wendongfly/myhi 1.0.9 → 1.0.11
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/dist/attach.js +1 -187
- package/dist/index.js +5 -5
- package/package.json +1 -1
package/dist/attach.js
CHANGED
|
@@ -1,188 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
* myhi attach — 将本地终端连接到运行中的 PTY 会话
|
|
4
|
-
*
|
|
5
|
-
* 用法:
|
|
6
|
-
* node src/attach.js # 列出会话,然后选择
|
|
7
|
-
* node src/attach.js <id> # 直接附加到指定会话
|
|
8
|
-
*/
|
|
9
|
-
import { createRequire } from 'module';
|
|
10
|
-
import { readFileSync } from 'fs';
|
|
11
|
-
import { join } from 'path';
|
|
12
|
-
import { homedir } from 'os';
|
|
13
|
-
import { createInterface } from 'readline';
|
|
14
|
-
|
|
15
|
-
const require = createRequire(import.meta.url);
|
|
16
|
-
const { io } = require('socket.io-client');
|
|
17
|
-
|
|
18
|
-
const SERVER = process.env.MYHI_SERVER || 'http://localhost:3000';
|
|
19
|
-
const TOKEN = readFileSync(join(homedir(), '.myhi', 'token'), 'utf8').trim();
|
|
20
|
-
|
|
21
|
-
function cleanup(socket) {
|
|
22
|
-
try { process.stdin.setRawMode(false); } catch {}
|
|
23
|
-
process.stdin.pause();
|
|
24
|
-
socket.disconnect();
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function attach(socket, sessionId) {
|
|
28
|
-
socket.emit('join', sessionId);
|
|
29
|
-
|
|
30
|
-
socket.on('joined', (session) => {
|
|
31
|
-
process.stderr.write(`\r\n[myhi] 已附加到 "${session.title}" (${sessionId})\r\n`);
|
|
32
|
-
process.stderr.write('[myhi] 按 Ctrl+] 分离\r\n\r\n');
|
|
33
|
-
|
|
34
|
-
// 本地终端自动获取控制权
|
|
35
|
-
socket.emit('take-control', { sessionId });
|
|
36
|
-
|
|
37
|
-
try { process.stdin.setRawMode(true); } catch {}
|
|
38
|
-
process.stdin.resume();
|
|
39
|
-
process.stdin.setEncoding('binary');
|
|
40
|
-
|
|
41
|
-
// 标准输入 → PTY
|
|
42
|
-
process.stdin.on('data', (data) => {
|
|
43
|
-
// Ctrl+] (0x1d) = 分离
|
|
44
|
-
if (data === '\x1d') {
|
|
45
|
-
process.stderr.write('\r\n[myhi] 已分离\r\n');
|
|
46
|
-
cleanup(socket);
|
|
47
|
-
process.exit(0);
|
|
48
|
-
}
|
|
49
|
-
socket.emit('input', data);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
// PTY → 标准输出
|
|
53
|
-
socket.on('output', (data) => {
|
|
54
|
-
process.stdout.write(data, 'binary');
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
// 仅在本地窗口实际调整大小时同步尺寸,
|
|
58
|
-
// 避免触发 PTY 重绘导致输出重复
|
|
59
|
-
process.stdout.on('resize', () => {
|
|
60
|
-
socket.emit('resize', {
|
|
61
|
-
cols: process.stdout.columns || 80,
|
|
62
|
-
rows: process.stdout.rows || 24,
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
// 控制权被拒绝
|
|
68
|
-
socket.on('control-denied', ({ reason }) => {
|
|
69
|
-
process.stderr.write(`\r\n[myhi] 获取控制权失败: ${reason}\r\n`);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
// 控制权被其他用户获取
|
|
73
|
-
socket.on('control-changed', ({ holder, holderName }) => {
|
|
74
|
-
if (holder && holder !== socket.id) {
|
|
75
|
-
process.stderr.write(`\r\n[myhi] ${holderName || '其他用户'} 已获取控制权,当前为只读\r\n`);
|
|
76
|
-
} else if (!holder) {
|
|
77
|
-
process.stderr.write('\r\n[myhi] 控制权已释放\r\n');
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
socket.on('session-exit', ({ code }) => {
|
|
82
|
-
process.stderr.write(`\r\n[myhi] 会话已退出 (code ${code})\r\n`);
|
|
83
|
-
cleanup(socket);
|
|
84
|
-
process.exit(0);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
socket.on('error', ({ message }) => {
|
|
88
|
-
process.stderr.write(`\r\n[myhi] 错误: ${message}\r\n`);
|
|
89
|
-
cleanup(socket);
|
|
90
|
-
process.exit(1);
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async function pickSession(socket) {
|
|
95
|
-
return new Promise((resolve, reject) => {
|
|
96
|
-
socket.emit('list');
|
|
97
|
-
socket.once('sessions', (sessions) => {
|
|
98
|
-
const alive = sessions.filter(s => s.alive);
|
|
99
|
-
if (!alive.length) {
|
|
100
|
-
process.stderr.write('[myhi] 没有活跃的会话。\n');
|
|
101
|
-
cleanup(socket);
|
|
102
|
-
process.exit(0);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
process.stdout.write('\n活跃会话:\n');
|
|
106
|
-
alive.forEach((s, i) => {
|
|
107
|
-
const viewers = s.viewers > 0 ? ` (${s.viewers} 人在线)` : '';
|
|
108
|
-
process.stdout.write(` [${i + 1}] ${s.title}${viewers} — ${s.id}\n`);
|
|
109
|
-
});
|
|
110
|
-
process.stdout.write('\n');
|
|
111
|
-
|
|
112
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
113
|
-
rl.question('选择会话编号: ', (answer) => {
|
|
114
|
-
rl.close();
|
|
115
|
-
const idx = parseInt(answer, 10) - 1;
|
|
116
|
-
if (idx < 0 || idx >= alive.length) {
|
|
117
|
-
process.stderr.write('[myhi] 无效的选择。\n');
|
|
118
|
-
cleanup(socket);
|
|
119
|
-
process.exit(1);
|
|
120
|
-
}
|
|
121
|
-
resolve(alive[idx].id);
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
async function createAndAttach(socket, opts) {
|
|
128
|
-
return new Promise((resolve, reject) => {
|
|
129
|
-
socket.emit('create', opts, (res) => {
|
|
130
|
-
if (!res?.ok) {
|
|
131
|
-
process.stderr.write(`[myhi] 创建失败: ${res?.error || '未知错误'}\n`);
|
|
132
|
-
cleanup(socket);
|
|
133
|
-
process.exit(1);
|
|
134
|
-
}
|
|
135
|
-
process.stdout.write(`[myhi] 已创建会话 "${res.session.title}" — ${res.session.id}\n`);
|
|
136
|
-
resolve(res.session.id);
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
async function promptNew(socket) {
|
|
142
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
143
|
-
const ask = (q) => new Promise((r) => rl.question(q, r));
|
|
144
|
-
const title = (await ask('会话名称 [shell]: ')).trim() || 'shell';
|
|
145
|
-
const initCmd = (await ask('启动命令(可选): ')).trim() || undefined;
|
|
146
|
-
rl.close();
|
|
147
|
-
return createAndAttach(socket, { title, initCmd });
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// ── 主入口 ──────────────────────────────────────────────
|
|
151
|
-
|
|
152
|
-
const socket = io(SERVER, {
|
|
153
|
-
transports: ['websocket'],
|
|
154
|
-
auth: { token: TOKEN },
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
socket.on('connect_error', (err) => {
|
|
158
|
-
process.stderr.write(`[myhi] 连接失败: ${err.message}\n`);
|
|
159
|
-
process.exit(1);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
socket.on('connect', async () => {
|
|
163
|
-
const arg = process.argv[2];
|
|
164
|
-
|
|
165
|
-
if (arg === '--new') {
|
|
166
|
-
// --new [标题] [命令] → 创建后附加
|
|
167
|
-
const title = process.argv[3];
|
|
168
|
-
const initCmd = process.argv[4] || undefined;
|
|
169
|
-
const sessionId = title
|
|
170
|
-
? await createAndAttach(socket, { title, initCmd })
|
|
171
|
-
: await promptNew(socket);
|
|
172
|
-
attach(socket, sessionId);
|
|
173
|
-
} else {
|
|
174
|
-
const sessionId = arg || await pickSession(socket);
|
|
175
|
-
attach(socket, sessionId);
|
|
176
|
-
}
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
// 退出时恢复终端状态
|
|
180
|
-
for (const sig of ['SIGINT', 'SIGTERM']) {
|
|
181
|
-
process.on(sig, () => {
|
|
182
|
-
cleanup(socket);
|
|
183
|
-
process.exit(0);
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
process.on('exit', () => {
|
|
187
|
-
try { process.stdin.setRawMode(false); } catch {}
|
|
188
|
-
});
|
|
2
|
+
import{createRequire as e}from"module";if(typeof __nccwpck_require__!=="undefined")__nccwpck_require__.ab=new URL(".",import.meta.url).pathname.slice(import.meta.url.match(/^file:\/\/\/\w:/)?1:0,-1)+"/";var t={};const s=e(import.meta.url)("module");const r=e(import.meta.url)("fs");const o=e(import.meta.url)("path");const n=e(import.meta.url)("os");const i=e(import.meta.url)("readline");const c=(0,s.createRequire)(import.meta.url);const{io:a}=c("socket.io-client");const p=process.env.MYHI_SERVER||"http://localhost:3000";const d=(0,r.readFileSync)((0,o.join)((0,n.homedir)(),".myhi","token"),"utf8").trim();function cleanup(e){try{process.stdin.setRawMode(false)}catch{}process.stdin.pause();e.disconnect()}function attach(e,t){e.emit("join",t);e.on("joined",(s=>{process.stderr.write(`\r\n[myhi] 已附加到 "${s.title}" (${t})\r\n`);process.stderr.write("[myhi] 按 Ctrl+] 分离\r\n\r\n");e.emit("take-control",{sessionId:t});try{process.stdin.setRawMode(true)}catch{}process.stdin.resume();process.stdin.setEncoding("binary");process.stdin.on("data",(t=>{if(t===""){process.stderr.write("\r\n[myhi] 已分离\r\n");cleanup(e);process.exit(0)}e.emit("input",t)}));e.on("output",(e=>{process.stdout.write(e,"binary")}));process.stdout.on("resize",(()=>{e.emit("resize",{cols:process.stdout.columns||80,rows:process.stdout.rows||24})}))}));e.on("control-denied",(({reason:e})=>{process.stderr.write(`\r\n[myhi] 获取控制权失败: ${e}\r\n`)}));e.on("control-changed",(({holder:t,holderName:s})=>{if(t&&t!==e.id){process.stderr.write(`\r\n[myhi] ${s||"其他用户"} 已获取控制权,当前为只读\r\n`)}else if(!t){process.stderr.write("\r\n[myhi] 控制权已释放\r\n")}}));e.on("session-exit",(({code:t})=>{process.stderr.write(`\r\n[myhi] 会话已退出 (code ${t})\r\n`);cleanup(e);process.exit(0)}));e.on("error",(({message:t})=>{process.stderr.write(`\r\n[myhi] 错误: ${t}\r\n`);cleanup(e);process.exit(1)}))}async function pickSession(e){return new Promise(((t,s)=>{e.emit("list");e.once("sessions",(s=>{const r=s.filter((e=>e.alive));if(!r.length){process.stderr.write("[myhi] 没有活跃的会话。\n");cleanup(e);process.exit(0)}process.stdout.write("\n活跃会话:\n");r.forEach(((e,t)=>{const s=e.viewers>0?` (${e.viewers} 人在线)`:"";process.stdout.write(` [${t+1}] ${e.title}${s} — ${e.id}\n`)}));process.stdout.write("\n");const o=(0,i.createInterface)({input:process.stdin,output:process.stdout});o.question("选择会话编号: ",(s=>{o.close();const n=parseInt(s,10)-1;if(n<0||n>=r.length){process.stderr.write("[myhi] 无效的选择。\n");cleanup(e);process.exit(1)}t(r[n].id)}))}))}))}async function createAndAttach(e,t){return new Promise(((s,r)=>{e.emit("create",t,(t=>{if(!t?.ok){process.stderr.write(`[myhi] 创建失败: ${t?.error||"未知错误"}\n`);cleanup(e);process.exit(1)}process.stdout.write(`[myhi] 已创建会话 "${t.session.title}" — ${t.session.id}\n`);s(t.session.id)}))}))}async function promptNew(e){const t=(0,i.createInterface)({input:process.stdin,output:process.stdout});const ask=e=>new Promise((s=>t.question(e,s)));const s=(await ask("会话名称 [shell]: ")).trim()||"shell";const r=(await ask("启动命令(可选): ")).trim()||undefined;t.close();return createAndAttach(e,{title:s,initCmd:r})}const u=a(p,{transports:["websocket"],auth:{token:d}});u.on("connect_error",(e=>{process.stderr.write(`[myhi] 连接失败: ${e.message}\n`);process.exit(1)}));u.on("connect",(async()=>{const e=process.argv[2];if(e==="--new"){const e=process.argv[3];const t=process.argv[4]||undefined;const s=e?await createAndAttach(u,{title:e,initCmd:t}):await promptNew(u);attach(u,s)}else{const t=e||await pickSession(u);attach(u,t)}}));for(const e of["SIGINT","SIGTERM"]){process.on(e,(()=>{cleanup(u);process.exit(0)}))}process.on("exit",(()=>{try{process.stdin.setRawMode(false)}catch{}}));
|