quick-ssh-new 1.0.1 → 1.0.2
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/Quick-SSH.psm1 +4 -1
- package/package.json +1 -1
- package/qssh-tui.js +187 -23
package/Quick-SSH.psm1
CHANGED
|
@@ -27,7 +27,10 @@ function Get-QuickSSHHosts {
|
|
|
27
27
|
try {
|
|
28
28
|
$raw = Get-Content -Path $Script:ConfigFile -Raw -Encoding UTF8
|
|
29
29
|
if ([string]::IsNullOrWhiteSpace($raw)) { return @() }
|
|
30
|
-
|
|
30
|
+
$data = $raw | ConvertFrom-Json
|
|
31
|
+
# 兼容单个对象 { ... } 和数组 [{ ... }, { ... }]
|
|
32
|
+
if ($data -is [array]) { return $data }
|
|
33
|
+
return @($data)
|
|
31
34
|
} catch {
|
|
32
35
|
return @()
|
|
33
36
|
}
|
package/package.json
CHANGED
package/qssh-tui.js
CHANGED
|
@@ -18,6 +18,18 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
const blessed = require("blessed");
|
|
21
|
+
|
|
22
|
+
// 阻止 blessed 切换备用屏幕缓冲区,保留 PowerShell 透明背景
|
|
23
|
+
if (blessed.Program) {
|
|
24
|
+
blessed.Program.prototype.alternateBuffer = function (val, cb) {
|
|
25
|
+
if (typeof val === "function") { cb = val; }
|
|
26
|
+
if (typeof cb === "function") { cb(); }
|
|
27
|
+
return this;
|
|
28
|
+
};
|
|
29
|
+
if (blessed.Program.prototype.smcup) blessed.Program.prototype.smcup = function () { return this; };
|
|
30
|
+
if (blessed.Program.prototype.rmcup) blessed.Program.prototype.rmcup = function () { return this; };
|
|
31
|
+
}
|
|
32
|
+
const net = require("net");
|
|
21
33
|
const fs = require("fs");
|
|
22
34
|
const path = require("path");
|
|
23
35
|
const { spawn } = require("child_process");
|
|
@@ -42,13 +54,20 @@ function loadHosts() {
|
|
|
42
54
|
ensureConfig();
|
|
43
55
|
try {
|
|
44
56
|
const raw = fs.readFileSync(CONFIG_FILE, "utf-8").trim();
|
|
45
|
-
|
|
46
|
-
|
|
57
|
+
if (!raw) return [];
|
|
58
|
+
const data = JSON.parse(raw);
|
|
59
|
+
// 兼容单个对象 { ... } 和数组 [{ ... }, { ... }]
|
|
60
|
+
return Array.isArray(data) ? data : [data];
|
|
61
|
+
} catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
47
64
|
}
|
|
48
65
|
|
|
49
66
|
function saveHosts(hosts) {
|
|
50
67
|
ensureConfig();
|
|
51
|
-
|
|
68
|
+
// 确保始终保存为数组
|
|
69
|
+
const data = Array.isArray(hosts) ? hosts : [];
|
|
70
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 4), "utf-8");
|
|
52
71
|
}
|
|
53
72
|
|
|
54
73
|
// ============================================================
|
|
@@ -95,12 +114,15 @@ let inputBox;
|
|
|
95
114
|
let helpBox;
|
|
96
115
|
let confirmBox;
|
|
97
116
|
|
|
117
|
+
// 在线状态缓存
|
|
118
|
+
const hostStatus = {}; // { alias: "unknown" | "online" | "offline" }
|
|
119
|
+
|
|
98
120
|
let hosts = [];
|
|
99
121
|
let filteredHosts = [];
|
|
100
122
|
let filterText = "";
|
|
101
123
|
|
|
102
124
|
// 模式管理
|
|
103
|
-
const MODE = { NORMAL: 0, SEARCH: 1, ADD: 2, EXPORT: 3, IMPORT: 4, CONFIRM: 5, HELP: 6 };
|
|
125
|
+
const MODE = { NORMAL: 0, SEARCH: 1, ADD: 2, EXPORT: 3, IMPORT: 4, CONFIRM: 5, HELP: 6, RENAME: 7 };
|
|
104
126
|
let currentMode = MODE.NORMAL;
|
|
105
127
|
|
|
106
128
|
// ============================================================
|
|
@@ -115,16 +137,18 @@ const MODE_LABELS = {
|
|
|
115
137
|
[MODE.IMPORT]: "{yellow-fg}{bold} IMPORT {/bold}{/yellow-fg}",
|
|
116
138
|
[MODE.CONFIRM]: "{red-fg}{bold} CONFIRM {/bold}{/red-fg}",
|
|
117
139
|
[MODE.HELP]: "{white-fg}{bold} HELP {/bold}{/white-fg}",
|
|
140
|
+
[MODE.RENAME]: "{cyan-fg}{bold} RENAME {/bold}{/cyan-fg}",
|
|
118
141
|
};
|
|
119
142
|
|
|
120
143
|
const MODE_HINTS = {
|
|
121
|
-
[MODE.NORMAL]: " j/k ↑↓ g首 G尾 ↵连接 d删除 a添加 /搜索 e导出 i导入 r重命名 ?帮助 q退出",
|
|
144
|
+
[MODE.NORMAL]: " j/k ↑↓ g首 G尾 ↵连接 d删除 a添加 /搜索 e导出 i导入 r重命名 p检测 P全检 ?帮助 q退出",
|
|
122
145
|
[MODE.SEARCH]: " 输入关键词过滤 Enter确认 Esc取消",
|
|
123
146
|
[MODE.ADD]: " 格式: 别名 用户@主机:端口 [--key 路径] Enter确认 Esc取消",
|
|
124
147
|
[MODE.EXPORT]: " 输入导出文件路径 (默认: ~/.quickssh/export.json) Enter确认 Esc取消",
|
|
125
148
|
[MODE.IMPORT]: " 输入导入文件路径 Enter确认 Esc取消",
|
|
126
149
|
[MODE.CONFIRM]: " y确认 n取消",
|
|
127
150
|
[MODE.HELP]: " 按任意键返回",
|
|
151
|
+
[MODE.RENAME]: " 输入新别名 Enter确认 Esc取消",
|
|
128
152
|
};
|
|
129
153
|
|
|
130
154
|
function setMode(mode, inputValue) {
|
|
@@ -137,10 +161,11 @@ function setMode(mode, inputValue) {
|
|
|
137
161
|
if (mode === MODE.NORMAL) listBox.focus();
|
|
138
162
|
}
|
|
139
163
|
|
|
140
|
-
if (mode === MODE.SEARCH || mode === MODE.ADD || mode === MODE.EXPORT || mode === MODE.IMPORT) {
|
|
164
|
+
if (mode === MODE.SEARCH || mode === MODE.ADD || mode === MODE.EXPORT || mode === MODE.IMPORT || mode === MODE.RENAME) {
|
|
141
165
|
inputBox.show();
|
|
142
166
|
inputBox.setValue(inputValue || "");
|
|
143
167
|
inputBox.focus();
|
|
168
|
+
inputBox.readInput();
|
|
144
169
|
}
|
|
145
170
|
|
|
146
171
|
if (mode === MODE.CONFIRM) {
|
|
@@ -161,9 +186,16 @@ function refreshList(keepSelection) {
|
|
|
161
186
|
: [...hosts];
|
|
162
187
|
|
|
163
188
|
const prevIdx = listBox.selected;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
189
|
+
const statusIndicators = {
|
|
190
|
+
online: "{green-fg}●{/green-fg}",
|
|
191
|
+
offline: "{red-fg}○{/red-fg}",
|
|
192
|
+
unknown: "{yellow-fg}◌{/yellow-fg}",
|
|
193
|
+
};
|
|
194
|
+
listBox.setItems(filteredHosts.map(h => {
|
|
195
|
+
const sta = hostStatus[h.alias] || "unknown";
|
|
196
|
+
const indicator = statusIndicators[sta] || statusIndicators.unknown;
|
|
197
|
+
return `${indicator} ${h.alias.padEnd(14)} ${h.user}@${h.host}:${h.port}`;
|
|
198
|
+
}));
|
|
167
199
|
|
|
168
200
|
if (keepSelection && prevIdx < filteredHosts.length) {
|
|
169
201
|
listBox.select(prevIdx);
|
|
@@ -189,6 +221,12 @@ function updateDetail() {
|
|
|
189
221
|
}
|
|
190
222
|
|
|
191
223
|
const h = filteredHosts[idx];
|
|
224
|
+
const sta = hostStatus[h.alias] || "unknown";
|
|
225
|
+
const statusMap = {
|
|
226
|
+
online: "{green-fg}● 在线{/green-fg}",
|
|
227
|
+
offline: "{red-fg}○ 离线{/red-fg}",
|
|
228
|
+
unknown: "{yellow-fg}◌ 未检测{/yellow-fg}",
|
|
229
|
+
};
|
|
192
230
|
detailBox.setContent(`
|
|
193
231
|
{bold}{cyan-fg} 连接详情{/cyan-fg}{/bold}
|
|
194
232
|
|
|
@@ -197,14 +235,87 @@ function updateDetail() {
|
|
|
197
235
|
{bold}账号:{/bold} ${h.user}
|
|
198
236
|
{bold}端口:{/bold} ${h.port}
|
|
199
237
|
{bold}私钥:{/bold} ${h.key || "(默认)"}
|
|
238
|
+
{bold}状态:{/bold} ${statusMap[sta]}
|
|
200
239
|
|
|
201
240
|
{blue-fg}─────────────────────{/blue-fg}
|
|
202
|
-
{green-fg} Enter → 连接{/green-fg}
|
|
203
|
-
{
|
|
241
|
+
{green-fg} Enter → 连接{/green-fg} {red-fg} d → 删除{/red-fg}
|
|
242
|
+
{yellow-fg} p → 检测在线{/yellow-fg}
|
|
204
243
|
`);
|
|
205
244
|
screen.render();
|
|
206
245
|
}
|
|
207
246
|
|
|
247
|
+
// ============================================================
|
|
248
|
+
// 在线检测
|
|
249
|
+
// ============================================================
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* 检查单台服务器是否在线(TCP 连接 SSH 端口)
|
|
253
|
+
*/
|
|
254
|
+
function checkHost(alias) {
|
|
255
|
+
return new Promise((resolve) => {
|
|
256
|
+
const h = hosts.find(e => e.alias === alias);
|
|
257
|
+
if (!h) { resolve(false); return; }
|
|
258
|
+
|
|
259
|
+
hostStatus[alias] = "checking";
|
|
260
|
+
refreshList();
|
|
261
|
+
|
|
262
|
+
const sock = new net.Socket();
|
|
263
|
+
const port = h.port || 22;
|
|
264
|
+
const timeout = 3000; // 3s
|
|
265
|
+
|
|
266
|
+
sock.setTimeout(timeout);
|
|
267
|
+
sock.on("connect", () => {
|
|
268
|
+
sock.destroy();
|
|
269
|
+
hostStatus[alias] = "online";
|
|
270
|
+
refreshList();
|
|
271
|
+
resolve(true);
|
|
272
|
+
});
|
|
273
|
+
sock.on("error", () => {
|
|
274
|
+
sock.destroy();
|
|
275
|
+
hostStatus[alias] = "offline";
|
|
276
|
+
refreshList();
|
|
277
|
+
resolve(false);
|
|
278
|
+
});
|
|
279
|
+
sock.on("timeout", () => {
|
|
280
|
+
sock.destroy();
|
|
281
|
+
hostStatus[alias] = "offline";
|
|
282
|
+
refreshList();
|
|
283
|
+
resolve(false);
|
|
284
|
+
});
|
|
285
|
+
sock.connect(port, h.host);
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function checkSelectedHost() {
|
|
290
|
+
const idx = listBox.selected;
|
|
291
|
+
if (idx < 0 || idx >= filteredHosts.length) return;
|
|
292
|
+
const alias = filteredHosts[idx].alias;
|
|
293
|
+
flashMessage(`正在检测 '${alias}' ...`, "yellow");
|
|
294
|
+
checkHost(alias).then(ok => {
|
|
295
|
+
flashMessage(`'${alias}' ${ok ? "● 在线" : "○ 离线"}`, ok ? "green" : "red");
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function checkAllHosts() {
|
|
300
|
+
const list = filteredHosts.length > 0 ? filteredHosts : hosts;
|
|
301
|
+
if (list.length === 0) {
|
|
302
|
+
flashMessage("没有可检测的服务器", "red");
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
flashMessage(`正在检测 ${list.length} 台服务器 ...`, "yellow");
|
|
306
|
+
let done = 0;
|
|
307
|
+
for (const h of list) {
|
|
308
|
+
checkHost(h.alias).then(() => {
|
|
309
|
+
done++;
|
|
310
|
+
if (done === list.length) {
|
|
311
|
+
const online = list.filter(e => hostStatus[e.alias] === "online").length;
|
|
312
|
+
const offline = list.filter(e => hostStatus[e.alias] === "offline").length;
|
|
313
|
+
flashMessage(`检测完成: ${online} 在线, ${offline} 离线`, online > 0 ? "green" : "red");
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
208
319
|
// ============================================================
|
|
209
320
|
// 操作函数
|
|
210
321
|
// ============================================================
|
|
@@ -334,6 +445,28 @@ function handleInputSubmit(value) {
|
|
|
334
445
|
setMode(MODE.NORMAL);
|
|
335
446
|
break;
|
|
336
447
|
}
|
|
448
|
+
case MODE.RENAME: {
|
|
449
|
+
inputBox.hide();
|
|
450
|
+
const idx = listBox.selected;
|
|
451
|
+
if (idx < 0 || idx >= filteredHosts.length) { setMode(MODE.NORMAL); break; }
|
|
452
|
+
const oldAlias = filteredHosts[idx].alias;
|
|
453
|
+
if (!val || val.trim() === oldAlias) { setMode(MODE.NORMAL); break; }
|
|
454
|
+
const newAlias = val.trim();
|
|
455
|
+
if (hosts.find(h => h.alias === newAlias)) {
|
|
456
|
+
flashMessage(`别名 '${newAlias}' 已存在`, "red");
|
|
457
|
+
setMode(MODE.NORMAL);
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
const target = hosts.find(h => h.alias === oldAlias);
|
|
461
|
+
if (target) {
|
|
462
|
+
target.alias = newAlias;
|
|
463
|
+
saveHosts(hosts);
|
|
464
|
+
refreshList(true);
|
|
465
|
+
flashMessage(`已重命名 '${oldAlias}' → '${newAlias}'`, "green");
|
|
466
|
+
}
|
|
467
|
+
setMode(MODE.NORMAL);
|
|
468
|
+
break;
|
|
469
|
+
}
|
|
337
470
|
}
|
|
338
471
|
}
|
|
339
472
|
|
|
@@ -344,6 +477,7 @@ function handleInputCancel() {
|
|
|
344
477
|
case MODE.ADD: setMode(MODE.NORMAL); break;
|
|
345
478
|
case MODE.EXPORT: setMode(MODE.NORMAL); break;
|
|
346
479
|
case MODE.IMPORT: setMode(MODE.NORMAL); break;
|
|
480
|
+
case MODE.RENAME: setMode(MODE.NORMAL); break;
|
|
347
481
|
default: setMode(MODE.NORMAL); break;
|
|
348
482
|
}
|
|
349
483
|
}
|
|
@@ -366,7 +500,7 @@ function startTUI() {
|
|
|
366
500
|
top: 0, left: 0, width: "100%", height: 1,
|
|
367
501
|
content: "{bold} Quick-SSH {/bold}",
|
|
368
502
|
tags: true,
|
|
369
|
-
style: { fg: "white", bg:
|
|
503
|
+
style: { fg: "white", bg: -1 },
|
|
370
504
|
});
|
|
371
505
|
|
|
372
506
|
// 连接列表
|
|
@@ -375,11 +509,11 @@ function startTUI() {
|
|
|
375
509
|
label: " 连接列表 ",
|
|
376
510
|
border: { type: "line", fg: "cyan" },
|
|
377
511
|
tags: true, keys: false, vi: false, mouse: true,
|
|
378
|
-
scrollbar: { ch: " ", bg:
|
|
512
|
+
scrollbar: { ch: " ", bg: -1 },
|
|
379
513
|
style: {
|
|
380
|
-
fg: "white", bg:
|
|
514
|
+
fg: "white", bg: -1,
|
|
381
515
|
selected: { fg: "white", bg: "blue", bold: true },
|
|
382
|
-
item: { fg: "white", bg:
|
|
516
|
+
item: { fg: "white", bg: -1 },
|
|
383
517
|
},
|
|
384
518
|
});
|
|
385
519
|
|
|
@@ -388,14 +522,14 @@ function startTUI() {
|
|
|
388
522
|
top: 1, right: 0, width: "40%", height: "100%-2",
|
|
389
523
|
label: " 详情 ",
|
|
390
524
|
border: { type: "line", fg: "cyan" },
|
|
391
|
-
tags: true, style: { fg: "white", bg:
|
|
525
|
+
tags: true, style: { fg: "white", bg: -1 },
|
|
392
526
|
scrollable: true, alwaysScroll: true,
|
|
393
527
|
});
|
|
394
528
|
|
|
395
529
|
// 状态栏
|
|
396
530
|
statusBar = blessed.box({
|
|
397
531
|
bottom: 0, left: 0, width: "100%", height: 1,
|
|
398
|
-
tags: true, style: { fg: "white", bg:
|
|
532
|
+
tags: true, style: { fg: "white", bg: -1 },
|
|
399
533
|
});
|
|
400
534
|
|
|
401
535
|
// 输入框
|
|
@@ -404,7 +538,7 @@ function startTUI() {
|
|
|
404
538
|
label: " 输入 ",
|
|
405
539
|
border: { type: "line", fg: "cyan" },
|
|
406
540
|
hidden: true, keys: true, vi: false,
|
|
407
|
-
style: { fg: "white", bg:
|
|
541
|
+
style: { fg: "white", bg: -1, border: { fg: "cyan" } },
|
|
408
542
|
});
|
|
409
543
|
|
|
410
544
|
// 确认框
|
|
@@ -412,7 +546,7 @@ function startTUI() {
|
|
|
412
546
|
top: "50%-3", left: "30%", width: "40%", height: 3,
|
|
413
547
|
border: { type: "line", fg: "red" },
|
|
414
548
|
tags: true, hidden: true,
|
|
415
|
-
style: { fg: "white", bg:
|
|
549
|
+
style: { fg: "white", bg: -1 },
|
|
416
550
|
});
|
|
417
551
|
|
|
418
552
|
// 帮助弹窗
|
|
@@ -422,14 +556,14 @@ function startTUI() {
|
|
|
422
556
|
border: { type: "line", fg: "yellow" },
|
|
423
557
|
tags: true, hidden: true, scrollable: true, alwaysScroll: true,
|
|
424
558
|
keys: true, vi: false,
|
|
425
|
-
style: { fg: "white", bg:
|
|
559
|
+
style: { fg: "white", bg: -1 },
|
|
426
560
|
content: `
|
|
427
561
|
{bold}{yellow-fg}Quick-SSH TUI — 快捷键帮助{/yellow-fg}{/bold}
|
|
428
562
|
|
|
429
563
|
{cyan-fg}━━━━━━━━━ 导航 ━━━━━━━━━{/cyan-fg}
|
|
430
564
|
{green-fg}j{/green-fg} / {green-fg}↓{/green-fg} 向下移动
|
|
431
565
|
{green-fg}k{/green-fg} / {green-fg}↑{/green-fg} 向上移动
|
|
432
|
-
{green-
|
|
566
|
+
{green-ff}g{/green-fg} 跳转到第一个
|
|
433
567
|
{green-fg}G{/green-fg} 跳转到最后一个
|
|
434
568
|
|
|
435
569
|
{cyan-fg}━━━━━━━━━ 操作 ━━━━━━━━━{/cyan-fg}
|
|
@@ -439,6 +573,10 @@ function startTUI() {
|
|
|
439
573
|
{green-fg}/ {/green-fg} 搜索 / 筛选
|
|
440
574
|
{green-fg}Esc{/green-fg} 取消 / 返回普通模式
|
|
441
575
|
|
|
576
|
+
{cyan-fg}━━━━━━━━━ 在线检测 ━━━━━━━━━{/cyan-fg}
|
|
577
|
+
{green-fg}p{/green-fg} 检测选中服务器是否在线
|
|
578
|
+
{green-fg}P{/green-fg} / {green-fg}C-p{/green-fg} 检测全部服务器
|
|
579
|
+
|
|
442
580
|
{cyan-fg}━━━━━━━━━ 导入导出 ━━━━━━━━━{/cyan-fg}
|
|
443
581
|
{green-fg}e{/green-fg} 导出全部配置
|
|
444
582
|
{green-fg}i{/green-fg} 从 JSON 文件导入
|
|
@@ -464,7 +602,11 @@ function startTUI() {
|
|
|
464
602
|
// ============================================================
|
|
465
603
|
|
|
466
604
|
// ----- 全局键 -----
|
|
467
|
-
screen.key(["C-c"], () => {
|
|
605
|
+
screen.key(["C-c"], () => {
|
|
606
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
607
|
+
screen.destroy();
|
|
608
|
+
process.exit(0);
|
|
609
|
+
});
|
|
468
610
|
|
|
469
611
|
screen.key(["escape"], () => {
|
|
470
612
|
if (currentMode === MODE.HELP) {
|
|
@@ -479,7 +621,11 @@ function startTUI() {
|
|
|
479
621
|
|
|
480
622
|
// ----- NORMAL 模式 -----
|
|
481
623
|
screen.key(["q"], () => {
|
|
482
|
-
if (currentMode === MODE.NORMAL) {
|
|
624
|
+
if (currentMode === MODE.NORMAL) {
|
|
625
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
626
|
+
screen.destroy();
|
|
627
|
+
process.exit(0);
|
|
628
|
+
}
|
|
483
629
|
});
|
|
484
630
|
|
|
485
631
|
screen.key(["/"], () => {
|
|
@@ -514,6 +660,24 @@ function startTUI() {
|
|
|
514
660
|
if (currentMode === MODE.NORMAL) setMode(MODE.IMPORT);
|
|
515
661
|
});
|
|
516
662
|
|
|
663
|
+
screen.key(["p"], () => {
|
|
664
|
+
if (currentMode === MODE.NORMAL) checkSelectedHost();
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
// 全检: P(Shift) / S-p / C-p(备选,部分终端会拦截 Shift)
|
|
668
|
+
screen.key(["P", "S-p", "C-p"], () => {
|
|
669
|
+
if (currentMode === MODE.NORMAL) checkAllHosts();
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
screen.key(["r"], () => {
|
|
673
|
+
if (currentMode === MODE.NORMAL) {
|
|
674
|
+
const idx = listBox.selected;
|
|
675
|
+
if (idx >= 0 && idx < filteredHosts.length) {
|
|
676
|
+
setMode(MODE.RENAME, filteredHosts[idx].alias);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
|
|
517
681
|
screen.key(["?", "h"], () => {
|
|
518
682
|
if (currentMode === MODE.NORMAL) { helpBox.show(); helpBox.focus(); setMode(MODE.HELP); }
|
|
519
683
|
});
|