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 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
- return @($raw | ConvertFrom-Json)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quick-ssh-new",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "🚀 Quick-SSH - 仿 Docker 命令行风格的 PowerShell SSH 连接管理工具",
5
5
  "keywords": [
6
6
  "ssh",
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
- return raw ? JSON.parse(raw) : [];
46
- } catch { return []; }
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
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(hosts, null, 4), "utf-8");
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
- listBox.setItems(filteredHosts.map(h =>
165
- `${h.alias.padEnd(16)} ${h.user}@${h.host}:${h.port}`
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
- {red-fg} d删除{/red-fg}
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: "blue" },
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: "cyan" },
512
+ scrollbar: { ch: " ", bg: -1 },
379
513
  style: {
380
- fg: "white", bg: "black",
514
+ fg: "white", bg: -1,
381
515
  selected: { fg: "white", bg: "blue", bold: true },
382
- item: { fg: "white", bg: "black" },
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: "black" },
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: "black" },
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: "black", border: { fg: "cyan" } },
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: "black" },
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: "black" },
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-fg}g{/green-fg} 跳转到第一个
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"], () => { screen.destroy(); process.exit(0); });
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) { screen.destroy(); process.exit(0); }
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
  });