dsh-ssh-tunnel 1.0.0 → 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/CHANGELOG.md +68 -0
- package/README.md +80 -38
- package/README_en.md +80 -38
- package/cordis.patch.yml +4 -4
- package/icon.svg +26 -0
- package/lib/client.js +1643 -1472
- package/lib/index.js +1739 -1611
- package/lib/session.js +616 -525
- package/lib/shared/args.js +23 -0
- package/lib/shared/host-key.js +32 -32
- package/lib/shared/host-summary.js +75 -75
- package/lib/shared/http-trust.js +59 -59
- package/lib/shared/path.js +21 -22
- package/lib/shared/persist.js +41 -41
- package/lib/shared/session-auth.js +47 -39
- package/lib/shared/session-policy.js +151 -125
- package/lib/shared/shell-buffer.js +10 -10
- package/lib/shared/vendor.js +49 -49
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +91 -84
- package/scripts/install.ps1 +191 -93
- package/scripts/install.sh +278 -226
- package/scripts/lib/bundle-check.cjs +108 -0
- package/scripts/lib/strip-mount.cjs +114 -0
- package/scripts/lib/ws-exclude.cjs +143 -0
- package/scripts/smoke-test.mjs +899 -511
- package/scripts/sync-to-dsh.sh +87 -5
package/lib/client.js
CHANGED
|
@@ -1,1472 +1,1643 @@
|
|
|
1
|
-
window.__ModuleLoader__.load({
|
|
2
|
-
id: "dsh-ssh-tunnel",
|
|
3
|
-
factory: (require) => {
|
|
4
|
-
var module = { exports: {} };
|
|
5
|
-
var exports = module.exports;
|
|
6
|
-
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
-
const React = require("react");
|
|
8
|
-
|
|
9
|
-
// Lazy react-dom resolver (same module loader registry as react).
|
|
10
|
-
// Only needed for the body portal in CenterOverlay; optional on purpose.
|
|
11
|
-
let ReactDOMRef;
|
|
12
|
-
function getReactDOM() {
|
|
13
|
-
if (ReactDOMRef !== undefined) return ReactDOMRef;
|
|
14
|
-
try { ReactDOMRef = require("react-dom"); } catch (e) { ReactDOMRef = null; }
|
|
15
|
-
return ReactDOMRef;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// --- i18n (same pattern as dsh-better-sidebar) ---
|
|
19
|
-
const LOCALE_NS = "sshTunnel";
|
|
20
|
-
const zh = {
|
|
21
|
-
tabTitle: "SSH 隧道",
|
|
22
|
-
projectLabel: "项目: {path}",
|
|
23
|
-
projectUnbound: "(未绑定)",
|
|
24
|
-
refresh: "刷新",
|
|
25
|
-
tabGrants: "项目授权",
|
|
26
|
-
tabHosts: "主机库",
|
|
27
|
-
tabTunnel: "隧道会话",
|
|
28
|
-
needConfirm: "需要确认",
|
|
29
|
-
trustConnect: "信任并连接",
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
return
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
function loadScript(src) {
|
|
255
|
-
return new Promise(function (resolve, reject) {
|
|
256
|
-
const s = document.createElement("script");
|
|
257
|
-
s.src = src;
|
|
258
|
-
s.async = true;
|
|
259
|
-
s.onload = function () { resolve(); };
|
|
260
|
-
s.onerror = function () { reject(new Error("load fail " + src)); };
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
l
|
|
268
|
-
l.
|
|
269
|
-
l.
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function icon(size) {
|
|
306
|
-
const s = size || 16;
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
},
|
|
311
|
-
React.createElement("
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
".ssh-t-
|
|
353
|
-
".ssh-t-
|
|
354
|
-
".ssh-t-
|
|
355
|
-
".ssh-t-
|
|
356
|
-
".ssh-t-
|
|
357
|
-
".ssh-t-
|
|
358
|
-
".ssh-t-
|
|
359
|
-
".ssh-t-
|
|
360
|
-
".ssh-t-
|
|
361
|
-
|
|
362
|
-
".ssh-t-
|
|
363
|
-
".ssh-t-
|
|
364
|
-
".ssh-t-
|
|
365
|
-
".ssh-t-
|
|
366
|
-
|
|
367
|
-
".ssh-
|
|
368
|
-
".ssh-
|
|
369
|
-
".ssh-
|
|
370
|
-
".ssh-
|
|
371
|
-
".ssh-
|
|
372
|
-
".ssh-
|
|
373
|
-
".ssh-
|
|
374
|
-
".ssh-
|
|
375
|
-
".ssh-
|
|
376
|
-
".ssh-
|
|
377
|
-
".ssh-
|
|
378
|
-
".ssh-
|
|
379
|
-
".ssh-
|
|
380
|
-
".ssh-
|
|
381
|
-
".ssh-
|
|
382
|
-
".ssh-
|
|
383
|
-
".ssh-
|
|
384
|
-
".ssh-
|
|
385
|
-
".ssh-
|
|
386
|
-
".ssh-
|
|
387
|
-
".ssh-
|
|
388
|
-
".ssh-
|
|
389
|
-
|
|
390
|
-
".ssh-
|
|
391
|
-
".ssh-
|
|
392
|
-
".ssh-
|
|
393
|
-
".ssh-
|
|
394
|
-
".ssh-
|
|
395
|
-
".ssh-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
if (
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
React.
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
if (
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
await api("
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
}
|
|
950
|
-
if (
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
)
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
const
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
})
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "dsh-ssh-tunnel",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
const React = require("react");
|
|
8
|
+
|
|
9
|
+
// Lazy react-dom resolver (same module loader registry as react).
|
|
10
|
+
// Only needed for the body portal in CenterOverlay; optional on purpose.
|
|
11
|
+
let ReactDOMRef;
|
|
12
|
+
function getReactDOM() {
|
|
13
|
+
if (ReactDOMRef !== undefined) return ReactDOMRef;
|
|
14
|
+
try { ReactDOMRef = require("react-dom"); } catch (e) { ReactDOMRef = null; }
|
|
15
|
+
return ReactDOMRef;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// --- i18n (same pattern as dsh-better-sidebar) ---
|
|
19
|
+
const LOCALE_NS = "sshTunnel";
|
|
20
|
+
const zh = {
|
|
21
|
+
tabTitle: "SSH 隧道",
|
|
22
|
+
projectLabel: "项目: {path}",
|
|
23
|
+
projectUnbound: "(未绑定)",
|
|
24
|
+
refresh: "刷新",
|
|
25
|
+
tabGrants: "项目授权",
|
|
26
|
+
tabHosts: "主机库",
|
|
27
|
+
tabTunnel: "隧道会话",
|
|
28
|
+
needConfirm: "需要确认",
|
|
29
|
+
trustConnect: "信任并连接",
|
|
30
|
+
dismissPrompt: "不信任",
|
|
31
|
+
promptKeyUnknown: "未知主机指纹",
|
|
32
|
+
promptKeyChanged: "主机指纹已变更",
|
|
33
|
+
promptFpNew: "新指纹",
|
|
34
|
+
promptFpOld: "原指纹",
|
|
35
|
+
trustedNotConnected: "已保存信任,但未发起连接:",
|
|
36
|
+
grantsHint: "勾选后,当前项目下的对话可通过 SSHManager 与隧道使用这些主机。连接前须先完成项目授权(不会在连接时自动授权)。",
|
|
37
|
+
saveGrants: "保存项目授权",
|
|
38
|
+
newHost: "新建主机",
|
|
39
|
+
scanOpenSsh: "扫描 OpenSSH",
|
|
40
|
+
edit: "编辑",
|
|
41
|
+
delete: "删除",
|
|
42
|
+
deleteHostConfirm: "确定删除主机「{name}」?此操作不可撤销。",
|
|
43
|
+
import: "导入",
|
|
44
|
+
name: "名称 / 别名",
|
|
45
|
+
host: "Host",
|
|
46
|
+
port: "Port",
|
|
47
|
+
username: "Username",
|
|
48
|
+
authType: "认证方式",
|
|
49
|
+
authPrivateKey: "私钥",
|
|
50
|
+
authPassword: "密码",
|
|
51
|
+
privateKeyPath: "私钥路径",
|
|
52
|
+
privateKeyPem: "或粘贴 PEM",
|
|
53
|
+
passphrase: "私钥口令",
|
|
54
|
+
password: "密码",
|
|
55
|
+
save: "保存",
|
|
56
|
+
back: "返回",
|
|
57
|
+
sftpOnConnect: "连接时启用 SFTP",
|
|
58
|
+
connectSection: "连接",
|
|
59
|
+
connect: "连接",
|
|
60
|
+
needHostKey: "需要在提示中信任 host key",
|
|
61
|
+
sessionsSection: "会话(终端 / SFTP 在中央打开)",
|
|
62
|
+
noSessions: "暂无会话",
|
|
63
|
+
terminal: "终端",
|
|
64
|
+
sftp: "SFTP",
|
|
65
|
+
disconnect: "断开",
|
|
66
|
+
reconnect: "重连",
|
|
67
|
+
reconnecting: "重连中 ({attempt}/{max})",
|
|
68
|
+
disconnected: "已断开",
|
|
69
|
+
connected: "已连接",
|
|
70
|
+
sessionDropped: "会话已断开",
|
|
71
|
+
kbiUnsupported: "不支持键盘交互登录,请编辑主机改为密码或私钥",
|
|
72
|
+
kbiEditNotice: "该主机原为键盘交互认证,此方式已不再支持;请改选密码或私钥并重新输入凭据。",
|
|
73
|
+
closeSession: "关闭",
|
|
74
|
+
footerHint: "连接与授权在此侧栏;终端与 SFTP 在中央面板打开。",
|
|
75
|
+
bashTitle: "SSH 终端 · {name}",
|
|
76
|
+
close: "关闭",
|
|
77
|
+
bash: "Bash",
|
|
78
|
+
bashStatusReady: "connected · 直接在终端内输入,Ctrl+C 等快捷键可用",
|
|
79
|
+
bashStatusLoading: "loading",
|
|
80
|
+
bashStatusError: "error",
|
|
81
|
+
xtermLoadFailed: "无法加载 xterm",
|
|
82
|
+
transferring: "传输中…",
|
|
83
|
+
transferSkipped: "传输完成,跳过 {count} 个目录",
|
|
84
|
+
mkdirTitle: "新建文件夹",
|
|
85
|
+
mkdirLocation: "位置:{path}",
|
|
86
|
+
mkdirPlaceholder: "文件夹名称",
|
|
87
|
+
create: "创建",
|
|
88
|
+
renameTitle: "重命名",
|
|
89
|
+
confirmDeleteTitle: "确认删除",
|
|
90
|
+
confirmDeleteDesc: "将删除 {count} 项,此操作可能不可恢复。",
|
|
91
|
+
copiedPath: "已复制路径",
|
|
92
|
+
pathTitle: "路径",
|
|
93
|
+
localProject: "本地项目",
|
|
94
|
+
remoteDevice: "远端设备",
|
|
95
|
+
parentDir: "上级",
|
|
96
|
+
upload: "上传 →",
|
|
97
|
+
download: "← 下载",
|
|
98
|
+
sftpHint: "多选: Ctrl/⌘ 点击 · Shift 范围 · 拖拽跨栏传输 · 右键菜单",
|
|
99
|
+
open: "打开",
|
|
100
|
+
uploadToRemote: "上传到远端",
|
|
101
|
+
downloadToLocal: "下载到本地",
|
|
102
|
+
newFolder: "新建文件夹",
|
|
103
|
+
rename: "重命名",
|
|
104
|
+
copyPath: "复制路径",
|
|
105
|
+
ok: "确定",
|
|
106
|
+
cancel: "取消",
|
|
107
|
+
gotIt: "好的",
|
|
108
|
+
};
|
|
109
|
+
const en = {
|
|
110
|
+
tabTitle: "SSH Tunnel",
|
|
111
|
+
projectLabel: "Project: {path}",
|
|
112
|
+
projectUnbound: "(not bound)",
|
|
113
|
+
refresh: "Refresh",
|
|
114
|
+
tabGrants: "Project access",
|
|
115
|
+
tabHosts: "Hosts",
|
|
116
|
+
tabTunnel: "Sessions",
|
|
117
|
+
needConfirm: "Confirmation needed",
|
|
118
|
+
trustConnect: "Trust and connect",
|
|
119
|
+
dismissPrompt: "Dismiss",
|
|
120
|
+
promptKeyUnknown: "Unknown host key",
|
|
121
|
+
promptKeyChanged: "Host key changed",
|
|
122
|
+
promptFpNew: "New fingerprint",
|
|
123
|
+
promptFpOld: "Previous fingerprint",
|
|
124
|
+
trustedNotConnected: "Trust saved, but the connection was not attempted:",
|
|
125
|
+
grantsHint: "Checked hosts can be used by SSHManager and the tunnel for this project. Grant access before connecting (connect does not auto-authorize).",
|
|
126
|
+
saveGrants: "Save project access",
|
|
127
|
+
newHost: "New host",
|
|
128
|
+
scanOpenSsh: "Scan OpenSSH",
|
|
129
|
+
edit: "Edit",
|
|
130
|
+
delete: "Delete",
|
|
131
|
+
deleteHostConfirm: "Delete host \"{name}\"? This cannot be undone.",
|
|
132
|
+
import: "Import",
|
|
133
|
+
name: "Name / alias",
|
|
134
|
+
host: "Host",
|
|
135
|
+
port: "Port",
|
|
136
|
+
username: "Username",
|
|
137
|
+
authType: "Authentication",
|
|
138
|
+
authPrivateKey: "Private key",
|
|
139
|
+
authPassword: "Password",
|
|
140
|
+
privateKeyPath: "Private key path",
|
|
141
|
+
privateKeyPem: "Or paste PEM",
|
|
142
|
+
passphrase: "Key passphrase",
|
|
143
|
+
password: "Password",
|
|
144
|
+
save: "Save",
|
|
145
|
+
back: "Back",
|
|
146
|
+
sftpOnConnect: "Enable SFTP on connect",
|
|
147
|
+
connectSection: "Connect",
|
|
148
|
+
connect: "Connect",
|
|
149
|
+
needHostKey: "Trust the host key in the prompt first",
|
|
150
|
+
sessionsSection: "Sessions (terminal / SFTP open in the center)",
|
|
151
|
+
noSessions: "No sessions",
|
|
152
|
+
terminal: "Terminal",
|
|
153
|
+
sftp: "SFTP",
|
|
154
|
+
disconnect: "Disconnect",
|
|
155
|
+
reconnect: "Reconnect",
|
|
156
|
+
reconnecting: "Reconnecting ({attempt}/{max})",
|
|
157
|
+
disconnected: "Disconnected",
|
|
158
|
+
connected: "Connected",
|
|
159
|
+
sessionDropped: "Session disconnected",
|
|
160
|
+
kbiUnsupported: "Keyboard-interactive auth is not supported; edit the host to password or private key",
|
|
161
|
+
kbiEditNotice: "This host used keyboard-interactive auth, which is no longer supported. Pick password or private key and re-enter the credential.",
|
|
162
|
+
closeSession: "Close",
|
|
163
|
+
footerHint: "Use this sidebar for connect and access; terminal and SFTP open in the center panel.",
|
|
164
|
+
bashTitle: "SSH terminal · {name}",
|
|
165
|
+
close: "Close",
|
|
166
|
+
bash: "Bash",
|
|
167
|
+
bashStatusReady: "connected · type directly in the terminal (Ctrl+C etc.)",
|
|
168
|
+
bashStatusLoading: "loading",
|
|
169
|
+
bashStatusError: "error",
|
|
170
|
+
xtermLoadFailed: "Failed to load xterm",
|
|
171
|
+
transferring: "Transferring…",
|
|
172
|
+
transferSkipped: "Transfer finished; {count} skipped",
|
|
173
|
+
mkdirTitle: "New folder",
|
|
174
|
+
mkdirLocation: "Location: {path}",
|
|
175
|
+
mkdirPlaceholder: "Folder name",
|
|
176
|
+
create: "Create",
|
|
177
|
+
renameTitle: "Rename",
|
|
178
|
+
confirmDeleteTitle: "Confirm delete",
|
|
179
|
+
confirmDeleteDesc: "Delete {count} item(s)? This may not be recoverable.",
|
|
180
|
+
copiedPath: "Path copied",
|
|
181
|
+
pathTitle: "Path",
|
|
182
|
+
localProject: "Local project",
|
|
183
|
+
remoteDevice: "Remote",
|
|
184
|
+
parentDir: "Up",
|
|
185
|
+
upload: "Upload →",
|
|
186
|
+
download: "← Download",
|
|
187
|
+
sftpHint: "Multi-select: Ctrl/⌘ click · Shift range · drag across panes · right-click menu",
|
|
188
|
+
open: "Open",
|
|
189
|
+
uploadToRemote: "Upload to remote",
|
|
190
|
+
downloadToLocal: "Download to local",
|
|
191
|
+
newFolder: "New folder",
|
|
192
|
+
rename: "Rename",
|
|
193
|
+
copyPath: "Copy path",
|
|
194
|
+
ok: "OK",
|
|
195
|
+
cancel: "Cancel",
|
|
196
|
+
gotIt: "OK",
|
|
197
|
+
};
|
|
198
|
+
let localeService;
|
|
199
|
+
function attachLocale(service) {
|
|
200
|
+
localeService = service;
|
|
201
|
+
}
|
|
202
|
+
function activeLocale() {
|
|
203
|
+
const fromSvc = localeService && localeService.getSnapshot ? localeService.getSnapshot().active : undefined;
|
|
204
|
+
const raw = fromSvc || (typeof navigator !== "undefined" ? navigator.language : "") || "en";
|
|
205
|
+
return String(raw);
|
|
206
|
+
}
|
|
207
|
+
function isZh() {
|
|
208
|
+
return activeLocale().toLowerCase().startsWith("zh");
|
|
209
|
+
}
|
|
210
|
+
/** Translate a copy key; `{name}` placeholders from params (better-sidebar style). */
|
|
211
|
+
function t(key, params) {
|
|
212
|
+
const dict = isZh() ? zh : en;
|
|
213
|
+
let text = dict[key];
|
|
214
|
+
if (text === undefined) text = en[key] || zh[key] || key;
|
|
215
|
+
if (params !== undefined && params !== null) {
|
|
216
|
+
for (const [name, value] of Object.entries(params)) {
|
|
217
|
+
text = String(text).split("{" + name + "}").join(String(value));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return text;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const TAB_ID = "dsh-ssh-tunnel";
|
|
224
|
+
const API = "/dsh-ssh-tunnel/api";
|
|
225
|
+
|
|
226
|
+
// Errors carry the structured fields the host sends: `code` (API_ERROR_STATUS
|
|
227
|
+
// semantic) and `data` (full parsed body) so callers can react to
|
|
228
|
+
// interactive states (e.g. a host-key prompt arrives as 200 + ok:false +
|
|
229
|
+
// promptId) instead of parsing message strings.
|
|
230
|
+
async function api(method, body) {
|
|
231
|
+
const response = await fetch(API + "/" + method, {
|
|
232
|
+
method: "POST",
|
|
233
|
+
headers: { "content-type": "application/json" },
|
|
234
|
+
body: JSON.stringify(body || {}),
|
|
235
|
+
});
|
|
236
|
+
const text = await response.text();
|
|
237
|
+
let data;
|
|
238
|
+
try { data = text ? JSON.parse(text) : {}; }
|
|
239
|
+
catch (e) { throw new Error("bad JSON (" + response.status + "): " + text.slice(0, 200)); }
|
|
240
|
+
const failed = !response.ok || (data && data.ok === false);
|
|
241
|
+
if (failed) {
|
|
242
|
+
const err = new Error(String((data && data.error) || ("HTTP " + response.status)));
|
|
243
|
+
if (data && data.code) err.code = String(data.code);
|
|
244
|
+
err.data = data;
|
|
245
|
+
throw err;
|
|
246
|
+
}
|
|
247
|
+
return data;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Injected vendor nodes are tracked so the caller's effect disposer can
|
|
251
|
+
// remove them again (no permanent <head> growth on plugin reload).
|
|
252
|
+
let xtermPromise = null;
|
|
253
|
+
const injectedVendor = [];
|
|
254
|
+
function loadScript(src) {
|
|
255
|
+
return new Promise(function (resolve, reject) {
|
|
256
|
+
const s = document.createElement("script");
|
|
257
|
+
s.src = src;
|
|
258
|
+
s.async = true;
|
|
259
|
+
s.onload = function () { resolve(); };
|
|
260
|
+
s.onerror = function () { s.remove(); reject(new Error("load fail " + src)); };
|
|
261
|
+
injectedVendor.push(s);
|
|
262
|
+
document.head.appendChild(s);
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
function loadCssOnce(href) {
|
|
266
|
+
if (document.querySelector('link[data-ssh-t="' + href + '"]')) return;
|
|
267
|
+
const l = document.createElement("link");
|
|
268
|
+
l.rel = "stylesheet";
|
|
269
|
+
l.href = href;
|
|
270
|
+
l.setAttribute("data-ssh-t", href);
|
|
271
|
+
injectedVendor.push(l);
|
|
272
|
+
document.head.appendChild(l);
|
|
273
|
+
}
|
|
274
|
+
// Vendor assets are same-origin files served by this plugin's host route
|
|
275
|
+
// (/dsh-ssh-tunnel/vendor/*). No CDN, no module-table dependency, and no
|
|
276
|
+
// require("@xterm/*") fallback — the module table never carries xterm, so
|
|
277
|
+
// that branch was dead code. A failed load retries on the next open
|
|
278
|
+
// (xtermPromise is reset below) instead of caching the failure forever.
|
|
279
|
+
async function ensureXterm() {
|
|
280
|
+
if (xtermPromise) return xtermPromise;
|
|
281
|
+
xtermPromise = (async function () {
|
|
282
|
+
try {
|
|
283
|
+
if (typeof document !== "undefined") {
|
|
284
|
+
loadCssOnce("/dsh-ssh-tunnel/vendor/xterm.css");
|
|
285
|
+
await loadScript("/dsh-ssh-tunnel/vendor/xterm.js");
|
|
286
|
+
await loadScript("/dsh-ssh-tunnel/vendor/addon-fit.js");
|
|
287
|
+
}
|
|
288
|
+
const Terminal = globalThis.Terminal || null;
|
|
289
|
+
const FitAddon = (globalThis.FitAddon && globalThis.FitAddon.FitAddon) || globalThis.FitAddon || null;
|
|
290
|
+
if (!Terminal) throw new Error(t("xtermLoadFailed"));
|
|
291
|
+
return { Terminal: Terminal, FitAddon: FitAddon, disposeVendor: disposeVendor };
|
|
292
|
+
} catch (e) {
|
|
293
|
+
xtermPromise = null; // allow a retry on the next open
|
|
294
|
+
throw e;
|
|
295
|
+
}
|
|
296
|
+
})();
|
|
297
|
+
return xtermPromise;
|
|
298
|
+
}
|
|
299
|
+
function disposeVendor() {
|
|
300
|
+
for (const node of injectedVendor.splice(0)) {
|
|
301
|
+
try { node.remove(); } catch (e) {}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function icon(size) {
|
|
306
|
+
const s = size || 16;
|
|
307
|
+
// 与包根 icon.svg 同一份图:插件列表磁贴与侧边栏共用同一张标。
|
|
308
|
+
return React.createElement("svg", {
|
|
309
|
+
width: s, height: s, viewBox: "0 0 36 36", fill: "none", "aria-hidden": true,
|
|
310
|
+
},
|
|
311
|
+
React.createElement("defs", null,
|
|
312
|
+
React.createElement("linearGradient", {
|
|
313
|
+
id: "dsh-ssh-tunnel-tab-host", x1: "18", y1: "6", x2: "18", y2: "30",
|
|
314
|
+
gradientUnits: "userSpaceOnUse",
|
|
315
|
+
},
|
|
316
|
+
React.createElement("stop", { stopColor: "#4E6EF2" }),
|
|
317
|
+
React.createElement("stop", { offset: "1", stopColor: "#1434B8" })),
|
|
318
|
+
React.createElement("linearGradient", {
|
|
319
|
+
id: "dsh-ssh-tunnel-tab-flow", x1: "14", y1: "18", x2: "22", y2: "18",
|
|
320
|
+
gradientUnits: "userSpaceOnUse",
|
|
321
|
+
},
|
|
322
|
+
React.createElement("stop", { stopColor: "#5CE1E6" }),
|
|
323
|
+
React.createElement("stop", { offset: "1", stopColor: "#38E298" }))),
|
|
324
|
+
React.createElement("g", { transform: "translate(18 18) scale(1.3) translate(-18 -18)" },
|
|
325
|
+
React.createElement("rect", { x: 7, y: 10, width: 7, height: 16, rx: 2, fill: "url(#dsh-ssh-tunnel-tab-host)" }),
|
|
326
|
+
React.createElement("circle", { cx: 10.5, cy: 14, r: 1, fill: "#5CE1E6" }),
|
|
327
|
+
React.createElement("circle", { cx: 10.5, cy: 18, r: 1, fill: "#FFFFFF", fillOpacity: 0.8 }),
|
|
328
|
+
React.createElement("rect", { x: 22, y: 10, width: 7, height: 16, rx: 2, fill: "url(#dsh-ssh-tunnel-tab-host)" }),
|
|
329
|
+
React.createElement("circle", { cx: 25.5, cy: 14, r: 1, fill: "#5CE1E6" }),
|
|
330
|
+
React.createElement("circle", { cx: 25.5, cy: 18, r: 1, fill: "#FFFFFF", fillOpacity: 0.8 }),
|
|
331
|
+
React.createElement("path", { d: "M14 15.5H22", stroke: "url(#dsh-ssh-tunnel-tab-flow)", strokeWidth: 2.2, strokeLinecap: "round" }),
|
|
332
|
+
React.createElement("path", { d: "M14 20.5H22", stroke: "url(#dsh-ssh-tunnel-tab-flow)", strokeWidth: 2.2, strokeLinecap: "round" }),
|
|
333
|
+
React.createElement("circle", { cx: 18, cy: 15.5, r: 1.3, fill: "#FFFFFF" }),
|
|
334
|
+
React.createElement("circle", { cx: 18, cy: 20.5, r: 1.3, fill: "#FFFFFF" })),
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Returns a disposer removing the injected <style>; apply() registers it
|
|
339
|
+
// through ctx.effect so a disabled plugin leaves no style rules behind.
|
|
340
|
+
function ensureStyles() {
|
|
341
|
+
if (typeof document === "undefined") return function () {};
|
|
342
|
+
// bump id when theme CSS changes so hard-refresh replaces rules
|
|
343
|
+
const STYLE_ID = "dsh-ssh-tunnel-style-v4";
|
|
344
|
+
const prev = document.getElementById("dsh-ssh-tunnel-style") || document.getElementById("dsh-ssh-tunnel-style-v2") || document.getElementById("dsh-ssh-tunnel-style-v3") || document.getElementById("dsh-ssh-tunnel-style-v4");
|
|
345
|
+
if (prev && prev.id === STYLE_ID && prev.getAttribute("data-rev") === "4") return;
|
|
346
|
+
if (prev) prev.remove();
|
|
347
|
+
const el = document.createElement("style");
|
|
348
|
+
el.id = STYLE_ID;
|
|
349
|
+
el.setAttribute("data-rev", "4");
|
|
350
|
+
el.textContent = [
|
|
351
|
+
/* Theme-only surfaces: no hard-coded black/gray panels */
|
|
352
|
+
".ssh-t-root{display:flex;flex-direction:column;gap:14px;padding:14px 14px 18px;height:100%;overflow:auto;font-size:13px;line-height:1.45;color:var(--dsw-alias-label-primary);box-sizing:border-box;background:transparent;}",
|
|
353
|
+
".ssh-t-head{display:flex;flex-wrap:wrap;align-items:flex-start;justify-content:space-between;gap:10px;}",
|
|
354
|
+
".ssh-t-title{font-weight:600;font-size:15px;letter-spacing:-0.01em;color:var(--dsw-alias-label-primary);}",
|
|
355
|
+
".ssh-t-sub{color:var(--dsw-alias-label-secondary);font-size:12px;word-break:break-all;margin-top:3px;opacity:1;}",
|
|
356
|
+
".ssh-t-tabs{display:flex;gap:4px;flex-wrap:wrap;padding:3px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);border-radius:999px;width:fit-content;}",
|
|
357
|
+
".ssh-t-tab{padding:6px 12px;border-radius:999px;border:none;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;font-size:12px;font-weight:500;transition:background var(--ds-transition-duration-slow, .15s) ease,color .15s ease;}",
|
|
358
|
+
".ssh-t-tab:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover);}",
|
|
359
|
+
".ssh-t-tab.on{background:var(--dsw-alias-button-primary-fill, var(--dsw-alias-brand-primary));color:var(--dsw-alias-label-primary-inverted, #fff);}",
|
|
360
|
+
".ssh-t-card{border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:14px;background:var(--dsw-alias-bg-layer-1);}",
|
|
361
|
+
/* Rows: keep action buttons visible when endpoint (IPv6) is long */
|
|
362
|
+
".ssh-t-row{display:flex;flex-wrap:wrap;align-items:flex-start;justify-content:space-between;gap:10px 12px;padding:12px 0;border-bottom:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-primary);}",
|
|
363
|
+
".ssh-t-row:last-child{border-bottom:none;padding-bottom:0;}",
|
|
364
|
+
".ssh-t-row:first-child{padding-top:0;}",
|
|
365
|
+
".ssh-t-row-main{flex:1 1 12rem;min-width:0;max-width:100%;}",
|
|
366
|
+
".ssh-t-row-main strong,.ssh-t-row-main .ssh-t-name{display:block;font-weight:600;overflow-wrap:anywhere;word-break:break-word;}",
|
|
367
|
+
".ssh-t-endpoint{display:block;color:var(--dsw-alias-label-secondary);font-size:12px;margin-top:2px;overflow-wrap:anywhere;word-break:break-word;line-height:1.4;}",
|
|
368
|
+
".ssh-t-row > .ssh-t-actions{flex:0 0 auto;margin-top:0;margin-left:auto;}",
|
|
369
|
+
".ssh-t-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px;align-items:center;}",
|
|
370
|
+
".ssh-t-check span{min-width:0;flex:1;overflow-wrap:anywhere;word-break:break-word;}",
|
|
371
|
+
".ssh-t-btn{padding:7px 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2, transparent);color:var(--dsw-alias-label-primary);cursor:pointer;font-size:12px;font-weight:500;transition:background .15s,border-color .15s;}",
|
|
372
|
+
".ssh-t-btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);border-color:var(--dsw-alias-border-l2);}",
|
|
373
|
+
".ssh-t-btn:disabled{opacity:.45;cursor:not-allowed;}",
|
|
374
|
+
".ssh-t-btn.primary{background:var(--dsw-alias-button-primary-fill, var(--dsw-alias-brand-primary));border-color:transparent;color:var(--dsw-alias-label-primary-inverted, #fff);}",
|
|
375
|
+
".ssh-t-btn.primary:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover, var(--dsw-alias-button-primary-fill));}",
|
|
376
|
+
".ssh-t-btn.danger{border-color:color-mix(in srgb, var(--dsw-alias-state-error-primary) 40%, transparent);color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);}",
|
|
377
|
+
".ssh-t-btn.danger:hover:not(:disabled){background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 16%, transparent);}",
|
|
378
|
+
".ssh-t-field{display:block;margin-bottom:12px;}",
|
|
379
|
+
".ssh-t-label{display:block;font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary);margin-bottom:6px;letter-spacing:0;text-transform:none;}",
|
|
380
|
+
".ssh-t-input,.ssh-t-select,.ssh-t-textarea{width:100%;box-sizing:border-box;padding:8px 10px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font-size:13px;outline:none;transition:border-color .15s, box-shadow .15s;}",
|
|
381
|
+
".ssh-t-input:focus,.ssh-t-select:focus,.ssh-t-textarea:focus{border-color:var(--dsw-alias-border-l4, var(--dsw-alias-brand-primary));box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-brand-primary) 22%, transparent);}",
|
|
382
|
+
".ssh-t-ok{color:var(--dsw-alias-state-success-primary, #3d9a5f);font-size:12px;}",
|
|
383
|
+
".ssh-t-err{color:var(--dsw-alias-state-error-primary);font-size:12px;white-space:pre-wrap;padding:8px 10px;border-radius:8px;background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);border:1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 28%, transparent);}",
|
|
384
|
+
".ssh-t-muted{color:var(--dsw-alias-label-secondary);font-size:12px;}",
|
|
385
|
+
".ssh-t-check{display:flex;align-items:center;gap:10px;margin:8px 0;cursor:pointer;padding:8px 10px;border-radius:8px;border:1px solid transparent;color:var(--dsw-alias-label-primary);}",
|
|
386
|
+
".ssh-t-check:hover{background:var(--dsw-alias-interactive-bg-hover);border-color:var(--dsw-alias-border-l1);}",
|
|
387
|
+
".ssh-t-check input{accent-color:var(--dsw-alias-brand-primary);}",
|
|
388
|
+
".ssh-t-prompt{border:1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary, #d97706) 40%, transparent);background:var(--dsw-alias-state-warn-tertiary, color-mix(in srgb, #d97706 12%, transparent));border-radius:12px;padding:12px 14px;color:var(--dsw-alias-label-primary);}",
|
|
389
|
+
/* Overlay follows theme; only terminal canvas stays dark for readability */
|
|
390
|
+
".ssh-ov-root{position:fixed;inset:0;z-index:12000;display:flex;align-items:center;justify-content:center;padding:24px;background:color-mix(in srgb, var(--dsw-alias-bg-base) 35%, rgba(0,0,0,.45));backdrop-filter:blur(6px);}",
|
|
391
|
+
".ssh-ov-panel{display:flex;flex-direction:column;position:relative;width:min(1180px,100%);height:min(840px,100%);background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:16px;box-shadow:0 18px 50px color-mix(in srgb, #000 35%, transparent);overflow:hidden;}",
|
|
392
|
+
".ssh-ov-bar{display:flex;align-items:center;gap:10px;padding:10px 14px;border-bottom:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);flex:none;}",
|
|
393
|
+
".ssh-ov-bar .ssh-t-title{flex:1;min-width:0;font-size:14px;color:var(--dsw-alias-label-primary);}",
|
|
394
|
+
".ssh-ov-tabs{display:flex;gap:4px;flex:none;padding:3px;background:var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-base));border-radius:999px;border:1px solid var(--dsw-alias-border-l1);}",
|
|
395
|
+
".ssh-ov-tabs button{padding:5px 12px;border-radius:999px;border:none;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;font-size:12px;font-weight:500;}",
|
|
396
|
+
".ssh-ov-tabs button.on{background:var(--dsw-alias-button-primary-fill, var(--dsw-alias-brand-primary));color:var(--dsw-alias-label-primary-inverted, #fff);}",
|
|
397
|
+
".ssh-ov-body{flex:1;min-height:0;display:flex;flex-direction:column;background:var(--dsw-alias-bg-base);}",
|
|
398
|
+
".ssh-ov-xterm{flex:1;min-height:0;padding:10px;background:#0b0f14;}",
|
|
399
|
+
".ssh-ov-xterm .xterm,.ssh-ov-xterm .xterm-viewport{height:100%;}",
|
|
400
|
+
".ssh-sftp{flex:1;min-height:0;display:flex;flex-direction:column;background:var(--dsw-alias-bg-base);}",
|
|
401
|
+
".ssh-sftp-panes{flex:1;min-height:0;display:grid;grid-template-columns:1fr 1fr;gap:0;}",
|
|
402
|
+
".ssh-sftp-pane{display:flex;flex-direction:column;min-width:0;min-height:0;border-right:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-base);}",
|
|
403
|
+
".ssh-sftp-pane:last-child{border-right:none;}",
|
|
404
|
+
".ssh-sftp-pane-hd{display:flex;flex-wrap:wrap;gap:8px;align-items:center;padding:10px 12px;border-bottom:1px solid var(--dsw-alias-border-l1);flex:none;background:var(--dsw-alias-bg-layer-1);}",
|
|
405
|
+
".ssh-sftp-pane-hd strong{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary);min-width:64px;}",
|
|
406
|
+
".ssh-sftp-pane-hd input{flex:1;min-width:80px;}",
|
|
407
|
+
".ssh-sftp-list{flex:1;min-height:0;overflow:auto;user-select:none;padding:6px;background:var(--dsw-alias-bg-base);}",
|
|
408
|
+
".ssh-sftp-item{display:flex;align-items:center;gap:10px;padding:8px 10px;font-size:12px;cursor:default;border-radius:8px;margin-bottom:2px;color:var(--dsw-alias-label-primary);}",
|
|
409
|
+
".ssh-sftp-item:hover{background:var(--dsw-alias-interactive-bg-hover);}",
|
|
410
|
+
".ssh-sftp-item.sel{background:var(--dsw-alias-interactive-bg-active, color-mix(in srgb, var(--dsw-alias-brand-primary) 16%, transparent));outline:1px solid color-mix(in srgb, var(--dsw-alias-brand-primary) 40%, transparent);}",
|
|
411
|
+
".ssh-sftp-item .nm{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
|
|
412
|
+
".ssh-sftp-item .sz{color:var(--dsw-alias-label-tertiary);flex:none;font-variant-numeric:tabular-nums;font-size:11px;}",
|
|
413
|
+
".ssh-ctx{position:fixed;z-index:13000;min-width:190px;background:var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-layer-1));border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:6px;box-shadow:0 12px 32px color-mix(in srgb, #000 25%, transparent);}",
|
|
414
|
+
".ssh-ctx button{display:block;width:100%;text-align:left;padding:8px 10px;border:none;background:transparent;color:var(--dsw-alias-label-primary);cursor:pointer;font-size:12px;border-radius:6px;}",
|
|
415
|
+
".ssh-ctx button:hover{background:var(--dsw-alias-interactive-bg-hover);}",
|
|
416
|
+
".ssh-ctx button.danger{color:var(--dsw-alias-state-error-primary);}",
|
|
417
|
+
".ssh-ctx hr{border:none;border-top:1px solid var(--dsw-alias-border-l1);margin:4px 0;}",
|
|
418
|
+
".ssh-modal-root{position:absolute;inset:0;z-index:20;display:flex;align-items:center;justify-content:center;padding:20px;background:color-mix(in srgb, var(--dsw-alias-bg-base) 30%, rgba(0,0,0,.35));}",".ssh-modal{width:min(400px,100%);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:14px;box-shadow:0 16px 40px color-mix(in srgb,#000 28%,transparent);padding:16px 16px 14px;}",".ssh-modal-title{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary);margin:0 0 6px;}",".ssh-modal-desc{font-size:12px;color:var(--dsw-alias-label-secondary);margin:0 0 12px;line-height:1.45;word-break:break-all;}",".ssh-modal-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:14px;}",".ssh-status{padding:7px 14px;font-size:11px;color:var(--dsw-alias-label-secondary);border-top:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);flex:none;}",
|
|
419
|
+
].join("\n");
|
|
420
|
+
document.head.appendChild(el);
|
|
421
|
+
return function () {
|
|
422
|
+
try { el.remove(); } catch (e) {}
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function Btn(props) {
|
|
427
|
+
const cls = "ssh-t-btn" + (props.primary ? " primary" : "") + (props.danger ? " danger" : "");
|
|
428
|
+
return React.createElement("button", { type: "button", className: cls, disabled: props.disabled, onClick: props.onClick }, props.children);
|
|
429
|
+
}
|
|
430
|
+
function Field(props) {
|
|
431
|
+
return React.createElement("label", { className: "ssh-t-field" },
|
|
432
|
+
React.createElement("span", { className: "ssh-t-label" }, props.label), props.children);
|
|
433
|
+
}
|
|
434
|
+
function emptyForm() {
|
|
435
|
+
return { id: "", name: "", host: "", port: 22, username: "", authType: "privateKey", privateKeyPath: "", password: "", privateKeyPem: "", passphrase: "" };
|
|
436
|
+
}
|
|
437
|
+
// Paths are dual-flavor: remote SFTP paths are POSIX, local paths on
|
|
438
|
+
// Windows carry backslashes. Split/join detect the flavor per path
|
|
439
|
+
// instead of assuming "/" everywhere (C-01).
|
|
440
|
+
function splitPath(p) {
|
|
441
|
+
return String(p || "").split(/[\\/]+/).filter(Boolean);
|
|
442
|
+
}
|
|
443
|
+
function parentOf(p) {
|
|
444
|
+
const parts = splitPath(p);
|
|
445
|
+
if (parts.length <= 1) {
|
|
446
|
+
// "C:\" style drive roots and "/" both have no parent
|
|
447
|
+
return /^[A-Za-z]:[\\/]?$/.test(String(p || "")) ? String(p) : "/";
|
|
448
|
+
}
|
|
449
|
+
const head = parts.slice(0, -1).join(p.indexOf("\\") >= 0 ? "\\" : "/");
|
|
450
|
+
// preserve the drive prefix ("C:" + "\x")
|
|
451
|
+
if (/^[A-Za-z]:$/.test(head)) return head + (p.indexOf("\\") >= 0 ? "\\" : "/");
|
|
452
|
+
return head || "/";
|
|
453
|
+
}
|
|
454
|
+
function joinPath(base, name) {
|
|
455
|
+
if (!base) return name;
|
|
456
|
+
const sep = String(base).indexOf("\\") >= 0 ? "\\" : "/";
|
|
457
|
+
if (base === "/" || /^[A-Za-z]:[\\/]?$/.test(String(base))) return base.replace(/[\\/]?$/, sep) + name;
|
|
458
|
+
return String(base).replace(/[\\/]+$/, "") + sep + name;
|
|
459
|
+
}
|
|
460
|
+
function formatSize(n) {
|
|
461
|
+
n = Number(n) || 0;
|
|
462
|
+
if (n < 1024) return n + " B";
|
|
463
|
+
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + " K";
|
|
464
|
+
return (n / (1024 * 1024)).toFixed(1) + " M";
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
/** In-panel dialog (replaces window.prompt / confirm). */
|
|
469
|
+
function TextDialog(props) {
|
|
470
|
+
const open = props.open;
|
|
471
|
+
const title = props.title || "";
|
|
472
|
+
const description = props.description || "";
|
|
473
|
+
const mode = props.mode || "prompt"; // prompt | confirm | alert
|
|
474
|
+
const confirmLabel = props.confirmLabel || (mode === "confirm" ? t("ok") : mode === "alert" ? t("gotIt") : t("ok"));
|
|
475
|
+
const cancelLabel = props.cancelLabel || t("cancel");
|
|
476
|
+
const danger = !!props.danger;
|
|
477
|
+
const [value, setValue] = React.useState(props.defaultValue || "");
|
|
478
|
+
const inputRef = React.useRef(null);
|
|
479
|
+
React.useEffect(function () {
|
|
480
|
+
if (!open) return;
|
|
481
|
+
setValue(props.defaultValue || "");
|
|
482
|
+
const t = setTimeout(function () {
|
|
483
|
+
if (inputRef.current) {
|
|
484
|
+
inputRef.current.focus();
|
|
485
|
+
inputRef.current.select && inputRef.current.select();
|
|
486
|
+
}
|
|
487
|
+
}, 30);
|
|
488
|
+
return function () { clearTimeout(t); };
|
|
489
|
+
}, [open, props.defaultValue, props.token]);
|
|
490
|
+
if (!open) return null;
|
|
491
|
+
function submit() {
|
|
492
|
+
if (mode === "prompt") props.onConfirm && props.onConfirm(value);
|
|
493
|
+
else props.onConfirm && props.onConfirm(true);
|
|
494
|
+
}
|
|
495
|
+
function cancel() {
|
|
496
|
+
props.onCancel && props.onCancel();
|
|
497
|
+
}
|
|
498
|
+
return React.createElement("div", {
|
|
499
|
+
className: "ssh-modal-root",
|
|
500
|
+
onMouseDown: function (e) {
|
|
501
|
+
if (e.target === e.currentTarget) cancel();
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
React.createElement("div", {
|
|
505
|
+
className: "ssh-modal",
|
|
506
|
+
role: "dialog",
|
|
507
|
+
"aria-modal": "true",
|
|
508
|
+
onMouseDown: function (e) { e.stopPropagation(); },
|
|
509
|
+
},
|
|
510
|
+
React.createElement("div", { className: "ssh-modal-title" }, title),
|
|
511
|
+
description ? React.createElement("div", { className: "ssh-modal-desc" }, description) : null,
|
|
512
|
+
mode === "prompt"
|
|
513
|
+
? React.createElement("input", {
|
|
514
|
+
ref: inputRef,
|
|
515
|
+
className: "ssh-t-input",
|
|
516
|
+
value: value,
|
|
517
|
+
placeholder: props.placeholder || "",
|
|
518
|
+
onChange: function (e) { setValue(e.target.value); },
|
|
519
|
+
onKeyDown: function (e) {
|
|
520
|
+
if (e.key === "Enter") submit();
|
|
521
|
+
if (e.key === "Escape") cancel();
|
|
522
|
+
},
|
|
523
|
+
})
|
|
524
|
+
: null,
|
|
525
|
+
React.createElement("div", { className: "ssh-modal-actions" },
|
|
526
|
+
mode !== "alert"
|
|
527
|
+
? React.createElement(Btn, { onClick: cancel }, cancelLabel)
|
|
528
|
+
: null,
|
|
529
|
+
React.createElement(Btn, {
|
|
530
|
+
primary: !danger,
|
|
531
|
+
danger: danger,
|
|
532
|
+
onClick: submit,
|
|
533
|
+
}, confirmLabel),
|
|
534
|
+
),
|
|
535
|
+
),
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
/** Full-screen center overlay host */
|
|
541
|
+
function CenterOverlay(props) {
|
|
542
|
+
const rootEl = React.createElement("div", {
|
|
543
|
+
className: "ssh-ov-root",
|
|
544
|
+
onMouseDown: function (e) {
|
|
545
|
+
if (e.target === e.currentTarget && props.onClose) props.onClose();
|
|
546
|
+
},
|
|
547
|
+
}, React.createElement("div", { className: "ssh-ov-panel", role: "dialog", "aria-modal": "true" }, props.children));
|
|
548
|
+
// Portal to <body>: the overlay relies on position:fixed being
|
|
549
|
+
// viewport-relative. better-sidebar >=0.16 gives its panel
|
|
550
|
+
// containers `contain: layout` (and panels animate with transform),
|
|
551
|
+
// which makes them the containing block for fixed descendants — an
|
|
552
|
+
// inline-rendered overlay would then be trapped inside the narrow
|
|
553
|
+
// sidebar instead of covering the screen. Portaling removes that
|
|
554
|
+
// dependency; fall back to inline rendering if react-dom is absent.
|
|
555
|
+
const rd = getReactDOM();
|
|
556
|
+
if (rd && typeof rd.createPortal === "function" && typeof document !== "undefined" && document.body) {
|
|
557
|
+
return rd.createPortal(rootEl, document.body);
|
|
558
|
+
}
|
|
559
|
+
return rootEl;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/** LA-like SSH terminal: xterm fills center, type directly */
|
|
563
|
+
function CenterBash(props) {
|
|
564
|
+
const { sessionId, projectPathKey, title, onClose, onOpenSftp } = props;
|
|
565
|
+
const hostRef = React.useRef(null);
|
|
566
|
+
const termRef = React.useRef(null);
|
|
567
|
+
const fitRef = React.useRef(null);
|
|
568
|
+
// Cursor into the host's {text,seq} chunk log: the last seq we rendered.
|
|
569
|
+
const sinceRef = React.useRef(0);
|
|
570
|
+
const [err, setErr] = React.useState("");
|
|
571
|
+
const [status, setStatus] = React.useState("loading");
|
|
572
|
+
|
|
573
|
+
React.useEffect(function () {
|
|
574
|
+
let cancelled = false;
|
|
575
|
+
let timer = null;
|
|
576
|
+
let ro = null;
|
|
577
|
+
let pollDelay = 120;
|
|
578
|
+
let inFlight = false;
|
|
579
|
+
// Terminal conditions carry structured codes from the host
|
|
580
|
+
// (API_ERROR_STATUS): the session was closed, expired, or its host
|
|
581
|
+
// grant was revoked. Everything else is transient: show it and
|
|
582
|
+
// back off instead of hammering a sick session every 120ms.
|
|
583
|
+
const isTerminalCode = function (e) {
|
|
584
|
+
return !!e && (e.code === "not_found" || e.code === "gone" || e.code === "forbidden");
|
|
585
|
+
};
|
|
586
|
+
const poll = async function () {
|
|
587
|
+
if (cancelled || !termRef.current || inFlight) return;
|
|
588
|
+
inFlight = true;
|
|
589
|
+
try {
|
|
590
|
+
const r = await api("shellRead", {
|
|
591
|
+
sessionId, projectPathKey, since: sinceRef.current,
|
|
592
|
+
});
|
|
593
|
+
if (cancelled || !termRef.current) return;
|
|
594
|
+
if (r.dropped) {
|
|
595
|
+
// Ring eviction swallowed output between our cursor and the
|
|
596
|
+
// retained base — a plain chunk would silently skip lines.
|
|
597
|
+
// Repaint from the retained base instead.
|
|
598
|
+
const resync = await api("shellRead", {
|
|
599
|
+
sessionId, projectPathKey, since: Math.max(0, (r.baseSeq || 1) - 1),
|
|
600
|
+
});
|
|
601
|
+
if (cancelled || !termRef.current) return;
|
|
602
|
+
try { termRef.current.clear(); } catch (e) {}
|
|
603
|
+
if (resync.chunk) termRef.current.write(resync.chunk);
|
|
604
|
+
sinceRef.current = resync.since != null ? resync.since : sinceRef.current;
|
|
605
|
+
} else {
|
|
606
|
+
if (r.chunk) termRef.current.write(r.chunk);
|
|
607
|
+
// Cursor = last seq actually delivered (`since`), not the
|
|
608
|
+
// buffer tail (`seq`): a chunkTruncated response continues
|
|
609
|
+
// from where this chunk stopped.
|
|
610
|
+
if (r.since != null) sinceRef.current = r.since;
|
|
611
|
+
else if (r.seq != null) sinceRef.current = r.seq;
|
|
612
|
+
if (r.chunkTruncated) console.debug("[dsh-ssh-tunnel] shell chunk hit the 256KiB cap; continuing next tick");
|
|
613
|
+
}
|
|
614
|
+
setErr("");
|
|
615
|
+
pollDelay = 120;
|
|
616
|
+
} catch (e) {
|
|
617
|
+
if (cancelled) return;
|
|
618
|
+
const msg = String(e && e.message ? e.message : e);
|
|
619
|
+
if (isTerminalCode(e)) {
|
|
620
|
+
stopPolling();
|
|
621
|
+
setErr(msg);
|
|
622
|
+
setStatus("dropped");
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
setErr(msg);
|
|
626
|
+
pollDelay = Math.min(pollDelay * 2, 2000);
|
|
627
|
+
} finally {
|
|
628
|
+
inFlight = false;
|
|
629
|
+
}
|
|
630
|
+
if (!cancelled) timer = setTimeout(poll, pollDelay);
|
|
631
|
+
};
|
|
632
|
+
const stopPolling = function () {
|
|
633
|
+
if (timer) { clearTimeout(timer); timer = null; }
|
|
634
|
+
};
|
|
635
|
+
(async function () {
|
|
636
|
+
try {
|
|
637
|
+
const mods = await ensureXterm();
|
|
638
|
+
if (cancelled || !hostRef.current) return;
|
|
639
|
+
if (!mods.Terminal) throw new Error(t("xtermLoadFailed"));
|
|
640
|
+
// disposeVendor on a previous close removed the stylesheet and
|
|
641
|
+
// the cached ensureXterm() promise never re-runs loadCssOnce —
|
|
642
|
+
// re-attach it here (idempotent via the data-ssh-t marker).
|
|
643
|
+
if (typeof document !== "undefined") loadCssOnce("/dsh-ssh-tunnel/vendor/xterm.css");
|
|
644
|
+
// First frame comes from shellOpen itself (it returns the whole
|
|
645
|
+
// buffer plus the seq cursor) — no second full-buffer fetch.
|
|
646
|
+
const open = await api("shellOpen", { sessionId, projectPathKey });
|
|
647
|
+
if (cancelled || !hostRef.current) return;
|
|
648
|
+
const term = new mods.Terminal({
|
|
649
|
+
cursorBlink: true,
|
|
650
|
+
fontSize: 14,
|
|
651
|
+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
|
|
652
|
+
theme: { background: "#0b0f14", foreground: "#d7e0ea", cursor: "#d7e0ea" },
|
|
653
|
+
convertEol: true,
|
|
654
|
+
scrollback: 5000,
|
|
655
|
+
});
|
|
656
|
+
let fit = null;
|
|
657
|
+
if (mods.FitAddon) {
|
|
658
|
+
fit = new mods.FitAddon();
|
|
659
|
+
term.loadAddon(fit);
|
|
660
|
+
}
|
|
661
|
+
term.open(hostRef.current);
|
|
662
|
+
if (fit) { try { fit.fit(); } catch (e) {} }
|
|
663
|
+
term.focus();
|
|
664
|
+
termRef.current = term;
|
|
665
|
+
fitRef.current = fit;
|
|
666
|
+
if (open.output) term.write(open.output);
|
|
667
|
+
sinceRef.current = open.seq != null ? open.seq : 0;
|
|
668
|
+
term.onData(function (data) {
|
|
669
|
+
api("shellWrite", { sessionId, projectPathKey, data: data }).catch(function (e) {
|
|
670
|
+
setErr(String(e && e.message ? e.message : e));
|
|
671
|
+
});
|
|
672
|
+
});
|
|
673
|
+
const sendSize = function () {
|
|
674
|
+
try {
|
|
675
|
+
if (fitRef.current) fitRef.current.fit();
|
|
676
|
+
} catch (e) {}
|
|
677
|
+
if (!termRef.current) return;
|
|
678
|
+
api("shellResize", {
|
|
679
|
+
sessionId, projectPathKey,
|
|
680
|
+
cols: termRef.current.cols,
|
|
681
|
+
rows: termRef.current.rows,
|
|
682
|
+
}).catch(function () {});
|
|
683
|
+
};
|
|
684
|
+
sendSize();
|
|
685
|
+
if (typeof ResizeObserver !== "undefined" && hostRef.current) {
|
|
686
|
+
ro = new ResizeObserver(sendSize);
|
|
687
|
+
ro.observe(hostRef.current);
|
|
688
|
+
}
|
|
689
|
+
window.addEventListener("resize", sendSize);
|
|
690
|
+
termRef.current._onWin = sendSize;
|
|
691
|
+
termRef.current._vendorDispose = mods.disposeVendor;
|
|
692
|
+
setStatus("connected");
|
|
693
|
+
timer = setTimeout(poll, pollDelay);
|
|
694
|
+
} catch (e) {
|
|
695
|
+
if (!cancelled) {
|
|
696
|
+
const msg = String(e && e.message ? e.message : e);
|
|
697
|
+
setErr(msg);
|
|
698
|
+
setStatus(isTerminalCode(e) || /not running|disconnected/i.test(msg) ? "dropped" : "error");
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
})();
|
|
702
|
+
return function () {
|
|
703
|
+
cancelled = true;
|
|
704
|
+
stopPolling();
|
|
705
|
+
if (ro) try { ro.disconnect(); } catch (e) {}
|
|
706
|
+
if (termRef.current) {
|
|
707
|
+
if (termRef.current._onWin) window.removeEventListener("resize", termRef.current._onWin);
|
|
708
|
+
if (termRef.current._vendorDispose) try { termRef.current._vendorDispose(); } catch (e) {}
|
|
709
|
+
try { termRef.current.dispose(); } catch (e) {}
|
|
710
|
+
termRef.current = null;
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
}, [sessionId, projectPathKey]);
|
|
714
|
+
|
|
715
|
+
return React.createElement(CenterOverlay, { onClose: onClose },
|
|
716
|
+
React.createElement("div", { className: "ssh-ov-bar" },
|
|
717
|
+
React.createElement("div", { className: "ssh-t-title" }, t("bashTitle", { name: title || sessionId.slice(0, 8) })),
|
|
718
|
+
React.createElement("div", { className: "ssh-ov-tabs" },
|
|
719
|
+
React.createElement("button", { type: "button", className: "on" }, t("bash")),
|
|
720
|
+
React.createElement("button", { type: "button", onClick: onOpenSftp }, t("sftp")),
|
|
721
|
+
),
|
|
722
|
+
React.createElement(Btn, { onClick: onClose }, t("close")),
|
|
723
|
+
),
|
|
724
|
+
React.createElement("div", { className: "ssh-ov-body" },
|
|
725
|
+
React.createElement("div", { className: "ssh-ov-xterm", ref: hostRef }),
|
|
726
|
+
),
|
|
727
|
+
React.createElement("div", { className: "ssh-status" }, (status === "connected" ? t("bashStatusReady") : status === "loading" ? t("bashStatusLoading") : status === "dropped" ? t("sessionDropped") : t("bashStatusError")) + (err ? " · " + err : "")),
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function FileList(props) {
|
|
732
|
+
const { entries, selected, onSelect, onActivate, onContext } = props;
|
|
733
|
+
return React.createElement("div", { className: "ssh-sftp-list", onContextMenu: function (e) {
|
|
734
|
+
// empty area
|
|
735
|
+
if (e.target === e.currentTarget) {
|
|
736
|
+
e.preventDefault();
|
|
737
|
+
onContext && onContext(e, null);
|
|
738
|
+
}
|
|
739
|
+
} },
|
|
740
|
+
entries.map(function (ent) {
|
|
741
|
+
const sel = selected.indexOf(ent.path) >= 0;
|
|
742
|
+
return React.createElement("div", {
|
|
743
|
+
key: ent.path,
|
|
744
|
+
className: "ssh-sftp-item" + (sel ? " sel" : ""),
|
|
745
|
+
onClick: function (e) { onSelect(ent, e); },
|
|
746
|
+
onDoubleClick: function () { onActivate(ent); },
|
|
747
|
+
onContextMenu: function (e) {
|
|
748
|
+
e.preventDefault();
|
|
749
|
+
e.stopPropagation();
|
|
750
|
+
onContext && onContext(e, ent);
|
|
751
|
+
},
|
|
752
|
+
draggable: true,
|
|
753
|
+
onDragStart: function (e) {
|
|
754
|
+
const paths = selected.indexOf(ent.path) >= 0 ? selected : [ent.path];
|
|
755
|
+
e.dataTransfer.setData("application/x-dsh-sftp", JSON.stringify({
|
|
756
|
+
side: props.side,
|
|
757
|
+
paths: paths,
|
|
758
|
+
}));
|
|
759
|
+
e.dataTransfer.effectAllowed = "copyMove";
|
|
760
|
+
},
|
|
761
|
+
},
|
|
762
|
+
React.createElement("span", null, ent.isDirectory ? "📁" : "📄"),
|
|
763
|
+
React.createElement("span", { className: "nm" }, ent.name),
|
|
764
|
+
React.createElement("span", { className: "sz" }, ent.isDirectory ? "" : formatSize(ent.size)),
|
|
765
|
+
);
|
|
766
|
+
}),
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/** LA-like dual-pane SFTP */
|
|
771
|
+
function CenterSftp(props) {
|
|
772
|
+
const { sessionId, projectPathKey, title, onClose, onOpenBash } = props;
|
|
773
|
+
const [localPath, setLocalPath] = React.useState(projectPathKey || "");
|
|
774
|
+
const [remotePath, setRemotePath] = React.useState("/");
|
|
775
|
+
const [localEntries, setLocalEntries] = React.useState([]);
|
|
776
|
+
const [remoteEntries, setRemoteEntries] = React.useState([]);
|
|
777
|
+
const [localSel, setLocalSel] = React.useState([]);
|
|
778
|
+
const [remoteSel, setRemoteSel] = React.useState([]);
|
|
779
|
+
const [status, setStatus] = React.useState("");
|
|
780
|
+
const [err, setErr] = React.useState("");
|
|
781
|
+
const [ctx, setCtx] = React.useState(null); // {x,y,side,entry|null}
|
|
782
|
+
const [dialog, setDialog] = React.useState(null); // in-panel modal config
|
|
783
|
+
const dialogToken = React.useRef(0);
|
|
784
|
+
const localAnchor = React.useRef(null);
|
|
785
|
+
const remoteAnchor = React.useRef(null);
|
|
786
|
+
|
|
787
|
+
function openDialog(cfg) {
|
|
788
|
+
dialogToken.current += 1;
|
|
789
|
+
return new Promise(function (resolve) {
|
|
790
|
+
setDialog(Object.assign({}, cfg, {
|
|
791
|
+
token: dialogToken.current,
|
|
792
|
+
_resolve: resolve,
|
|
793
|
+
}));
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
function finishDialog(result) {
|
|
797
|
+
setDialog(function (cur) {
|
|
798
|
+
if (cur && cur._resolve) cur._resolve(result);
|
|
799
|
+
return null;
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
function askText(opts) {
|
|
803
|
+
return openDialog({
|
|
804
|
+
mode: "prompt",
|
|
805
|
+
title: opts.title,
|
|
806
|
+
description: opts.description || "",
|
|
807
|
+
defaultValue: opts.defaultValue || "",
|
|
808
|
+
placeholder: opts.placeholder || "",
|
|
809
|
+
confirmLabel: opts.confirmLabel || t("ok"),
|
|
810
|
+
}).then(function (v) {
|
|
811
|
+
if (v == null) return null;
|
|
812
|
+
const s = String(v).trim();
|
|
813
|
+
return s ? s : null;
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
function askConfirm(opts) {
|
|
817
|
+
return openDialog({
|
|
818
|
+
mode: "confirm",
|
|
819
|
+
title: opts.title,
|
|
820
|
+
description: opts.description || "",
|
|
821
|
+
confirmLabel: opts.confirmLabel || t("ok"),
|
|
822
|
+
danger: !!opts.danger,
|
|
823
|
+
}).then(function (v) { return !!v; });
|
|
824
|
+
}
|
|
825
|
+
function askAlert(opts) {
|
|
826
|
+
return openDialog({
|
|
827
|
+
mode: "alert",
|
|
828
|
+
title: opts.title,
|
|
829
|
+
description: opts.description || "",
|
|
830
|
+
confirmLabel: opts.confirmLabel || t("gotIt"),
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
const refreshLocal = React.useCallback(async function (p) {
|
|
835
|
+
const path = p != null ? p : localPath;
|
|
836
|
+
const r = await api("localList", { projectPathKey, path: path });
|
|
837
|
+
setLocalPath(r.path);
|
|
838
|
+
setLocalEntries(r.entries || []);
|
|
839
|
+
setLocalSel([]);
|
|
840
|
+
}, [localPath, projectPathKey]);
|
|
841
|
+
|
|
842
|
+
const refreshRemote = React.useCallback(async function (p) {
|
|
843
|
+
const path = p != null ? p : remotePath;
|
|
844
|
+
const r = await api("sftpList", { sessionId, projectPathKey, path: path });
|
|
845
|
+
setRemotePath(r.path || path);
|
|
846
|
+
setRemoteEntries(r.entries || []);
|
|
847
|
+
setRemoteSel([]);
|
|
848
|
+
}, [remotePath, sessionId, projectPathKey]);
|
|
849
|
+
|
|
850
|
+
React.useEffect(function () {
|
|
851
|
+
refreshLocal(projectPathKey || "").catch(function (e) {
|
|
852
|
+
setErr(String(e && e.message ? e.message : e));
|
|
853
|
+
});
|
|
854
|
+
refreshRemote("/").catch(function (e) {
|
|
855
|
+
const msg = String(e && e.message ? e.message : e);
|
|
856
|
+
setErr(/not running|disconnected/i.test(msg) ? t("sessionDropped") + " · " + msg : msg);
|
|
857
|
+
});
|
|
858
|
+
}, [sessionId, projectPathKey]); // eslint-disable-line
|
|
859
|
+
|
|
860
|
+
function selectHandler(side) {
|
|
861
|
+
return function (ent, e) {
|
|
862
|
+
const list = side === "local" ? localEntries : remoteEntries;
|
|
863
|
+
const setSel = side === "local" ? setLocalSel : setRemoteSel;
|
|
864
|
+
const sel = side === "local" ? localSel : remoteSel;
|
|
865
|
+
const anchorRef = side === "local" ? localAnchor : remoteAnchor;
|
|
866
|
+
if (e.shiftKey && anchorRef.current) {
|
|
867
|
+
const paths = list.map(function (x) { return x.path; });
|
|
868
|
+
const a = paths.indexOf(anchorRef.current);
|
|
869
|
+
const b = paths.indexOf(ent.path);
|
|
870
|
+
if (a >= 0 && b >= 0) {
|
|
871
|
+
const lo = Math.min(a, b), hi = Math.max(a, b);
|
|
872
|
+
setSel(paths.slice(lo, hi + 1));
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
if (e.metaKey || e.ctrlKey) {
|
|
877
|
+
if (sel.indexOf(ent.path) >= 0) setSel(sel.filter(function (p) { return p !== ent.path; }));
|
|
878
|
+
else setSel(sel.concat([ent.path]));
|
|
879
|
+
anchorRef.current = ent.path;
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
setSel([ent.path]);
|
|
883
|
+
anchorRef.current = ent.path;
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function activate(side) {
|
|
888
|
+
return function (ent) {
|
|
889
|
+
if (ent.isDirectory) {
|
|
890
|
+
if (side === "local") refreshLocal(ent.path).catch(function (e) { setErr(String(e.message || e)); });
|
|
891
|
+
else refreshRemote(ent.path).catch(function (e) { setErr(String(e.message || e)); });
|
|
892
|
+
}
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
async function transfer(direction, paths) {
|
|
897
|
+
// direction: 'upload' local->remote, 'download' remote->local
|
|
898
|
+
setErr("");
|
|
899
|
+
setStatus(t("transferring"));
|
|
900
|
+
let skipped = 0;
|
|
901
|
+
try {
|
|
902
|
+
for (let i = 0; i < paths.length; i++) {
|
|
903
|
+
const src = paths[i];
|
|
904
|
+
const name = splitPath(src).pop();
|
|
905
|
+
if (direction === "upload") {
|
|
906
|
+
// skip dirs for v1 simple file upload
|
|
907
|
+
const ent = localEntries.find(function (e) { return e.path === src; });
|
|
908
|
+
if (ent && ent.isDirectory) {
|
|
909
|
+
skipped++;
|
|
910
|
+
continue;
|
|
911
|
+
}
|
|
912
|
+
const remote = joinPath(remotePath, name);
|
|
913
|
+
await api("sftpUpload", {
|
|
914
|
+
sessionId, projectPathKey,
|
|
915
|
+
local_path: src,
|
|
916
|
+
remote_path: remote,
|
|
917
|
+
});
|
|
918
|
+
} else {
|
|
919
|
+
const ent = remoteEntries.find(function (e) { return e.path === src; });
|
|
920
|
+
if (ent && ent.isDirectory) {
|
|
921
|
+
skipped++;
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
const local = joinPath(localPath, name);
|
|
925
|
+
await api("sftpDownload", {
|
|
926
|
+
sessionId, projectPathKey,
|
|
927
|
+
remote_path: src,
|
|
928
|
+
local_path: local,
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
await refreshLocal(localPath);
|
|
933
|
+
await refreshRemote(remotePath);
|
|
934
|
+
// C-09: a skipped directory used to overwrite the status with a
|
|
935
|
+
// per-item line and vanish; surface it as a final summary instead.
|
|
936
|
+
setStatus(skipped > 0 ? t("transferSkipped", { count: skipped }) : "");
|
|
937
|
+
} catch (e) {
|
|
938
|
+
setErr(String(e && e.message ? e.message : e));
|
|
939
|
+
setStatus("");
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
function onDropPane(side) {
|
|
944
|
+
return function (e) {
|
|
945
|
+
e.preventDefault();
|
|
946
|
+
let raw = e.dataTransfer.getData("application/x-dsh-sftp");
|
|
947
|
+
if (!raw) return;
|
|
948
|
+
let payload;
|
|
949
|
+
try { payload = JSON.parse(raw); } catch (err) { return; }
|
|
950
|
+
if (!payload || !payload.paths) return;
|
|
951
|
+
if (payload.side === side) return;
|
|
952
|
+
if (payload.side === "local" && side === "remote") transfer("upload", payload.paths);
|
|
953
|
+
if (payload.side === "remote" && side === "local") transfer("download", payload.paths);
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
async function ctxAction(action) {
|
|
958
|
+
if (!ctx) return;
|
|
959
|
+
const side = ctx.side;
|
|
960
|
+
const entry = ctx.entry;
|
|
961
|
+
const sel = side === "local" ? (localSel.length ? localSel : (entry ? [entry.path] : [])) : (remoteSel.length ? remoteSel : (entry ? [entry.path] : []));
|
|
962
|
+
setCtx(null);
|
|
963
|
+
try {
|
|
964
|
+
if (action === "open" && entry) {
|
|
965
|
+
activate(side)(entry);
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
if (action === "upload" && side === "local") {
|
|
969
|
+
await transfer("upload", sel);
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
if (action === "download" && side === "remote") {
|
|
973
|
+
await transfer("download", sel);
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
if (action === "mkdir") {
|
|
977
|
+
const name = await askText({
|
|
978
|
+
title: t("mkdirTitle"),
|
|
979
|
+
description: t("mkdirLocation", { path: side === "local" ? localPath : remotePath }),
|
|
980
|
+
placeholder: t("mkdirPlaceholder"),
|
|
981
|
+
confirmLabel: t("create"),
|
|
982
|
+
});
|
|
983
|
+
if (!name) return;
|
|
984
|
+
if (side === "local") {
|
|
985
|
+
await api("localMkdir", { projectPathKey, path: joinPath(localPath, name) });
|
|
986
|
+
await refreshLocal(localPath);
|
|
987
|
+
} else {
|
|
988
|
+
await api("sftpMkdir", { sessionId, projectPathKey, path: joinPath(remotePath, name) });
|
|
989
|
+
await refreshRemote(remotePath);
|
|
990
|
+
}
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
if (action === "rename" && entry) {
|
|
994
|
+
const name = await askText({
|
|
995
|
+
title: t("renameTitle"),
|
|
996
|
+
description: entry.path,
|
|
997
|
+
defaultValue: entry.name,
|
|
998
|
+
confirmLabel: t("save"),
|
|
999
|
+
});
|
|
1000
|
+
if (!name || name === entry.name) return;
|
|
1001
|
+
const dest = joinPath(side === "local" ? localPath : remotePath, name);
|
|
1002
|
+
if (side === "local") {
|
|
1003
|
+
await api("localRename", { projectPathKey, from_path: entry.path, to_path: dest });
|
|
1004
|
+
await refreshLocal(localPath);
|
|
1005
|
+
} else {
|
|
1006
|
+
await api("sftpRename", { sessionId, projectPathKey, from_path: entry.path, to_path: dest });
|
|
1007
|
+
await refreshRemote(remotePath);
|
|
1008
|
+
}
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
if (action === "delete") {
|
|
1012
|
+
if (!sel.length) return;
|
|
1013
|
+
const ok = await askConfirm({
|
|
1014
|
+
title: t("confirmDeleteTitle"),
|
|
1015
|
+
description: t("confirmDeleteDesc", { count: sel.length }),
|
|
1016
|
+
confirmLabel: t("delete"),
|
|
1017
|
+
danger: true,
|
|
1018
|
+
});
|
|
1019
|
+
if (!ok) return;
|
|
1020
|
+
for (let i = 0; i < sel.length; i++) {
|
|
1021
|
+
if (side === "local") await api("localDelete", { projectPathKey, path: sel[i] });
|
|
1022
|
+
else await api("sftpDelete", { sessionId, projectPathKey, path: sel[i] });
|
|
1023
|
+
}
|
|
1024
|
+
if (side === "local") await refreshLocal(localPath);
|
|
1025
|
+
else await refreshRemote(remotePath);
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
if (action === "copyPath" && entry) {
|
|
1029
|
+
try {
|
|
1030
|
+
await navigator.clipboard.writeText(entry.path);
|
|
1031
|
+
setStatus(t("copiedPath"));
|
|
1032
|
+
} catch (e) {
|
|
1033
|
+
await askAlert({ title: t("pathTitle"), description: entry.path });
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
} catch (e) {
|
|
1037
|
+
setErr(String(e && e.message ? e.message : e));
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function pane(side) {
|
|
1042
|
+
const isLocal = side === "local";
|
|
1043
|
+
const path = isLocal ? localPath : remotePath;
|
|
1044
|
+
const setPath = isLocal ? setLocalPath : setRemotePath;
|
|
1045
|
+
const entries = isLocal ? localEntries : remoteEntries;
|
|
1046
|
+
const sel = isLocal ? localSel : remoteSel;
|
|
1047
|
+
const refresh = isLocal ? refreshLocal : refreshRemote;
|
|
1048
|
+
return React.createElement("div", {
|
|
1049
|
+
className: "ssh-sftp-pane",
|
|
1050
|
+
onDragOver: function (e) { e.preventDefault(); },
|
|
1051
|
+
onDrop: onDropPane(side),
|
|
1052
|
+
},
|
|
1053
|
+
React.createElement("div", { className: "ssh-sftp-pane-hd" },
|
|
1054
|
+
React.createElement("strong", null, isLocal ? t("localProject") : t("remoteDevice")),
|
|
1055
|
+
React.createElement(Btn, { onClick: function () { refresh(parentOf(path)).catch(function (e) { setErr(String(e.message || e)); }); } }, t("parentDir")),
|
|
1056
|
+
React.createElement(Btn, { onClick: function () { refresh(path).catch(function (e) { setErr(String(e.message || e)); }); } }, t("refresh")),
|
|
1057
|
+
React.createElement("input", {
|
|
1058
|
+
className: "ssh-t-input",
|
|
1059
|
+
value: path,
|
|
1060
|
+
onChange: function (e) { setPath(e.target.value); },
|
|
1061
|
+
onKeyDown: function (e) {
|
|
1062
|
+
if (e.key === "Enter") refresh(path).catch(function (err) { setErr(String(err.message || err)); });
|
|
1063
|
+
},
|
|
1064
|
+
}),
|
|
1065
|
+
),
|
|
1066
|
+
React.createElement(FileList, {
|
|
1067
|
+
side: side,
|
|
1068
|
+
entries: entries,
|
|
1069
|
+
selected: sel,
|
|
1070
|
+
onSelect: selectHandler(side),
|
|
1071
|
+
onActivate: activate(side),
|
|
1072
|
+
onContext: function (e, ent) {
|
|
1073
|
+
// ensure selection includes entry
|
|
1074
|
+
if (ent) {
|
|
1075
|
+
const cur = isLocal ? localSel : remoteSel;
|
|
1076
|
+
if (cur.indexOf(ent.path) < 0) {
|
|
1077
|
+
if (isLocal) setLocalSel([ent.path]);
|
|
1078
|
+
else setRemoteSel([ent.path]);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
setCtx({ x: e.clientX, y: e.clientY, side: side, entry: ent });
|
|
1082
|
+
},
|
|
1083
|
+
}),
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
return React.createElement(CenterOverlay, { onClose: onClose },
|
|
1088
|
+
React.createElement("div", { className: "ssh-ov-bar" },
|
|
1089
|
+
React.createElement("div", { className: "ssh-t-title" }, t("sftp") + " · " + (title || sessionId.slice(0, 8))),
|
|
1090
|
+
React.createElement("div", { className: "ssh-ov-tabs" },
|
|
1091
|
+
React.createElement("button", { type: "button", onClick: onOpenBash }, t("bash")),
|
|
1092
|
+
React.createElement("button", { type: "button", className: "on" }, t("sftp")),
|
|
1093
|
+
),
|
|
1094
|
+
React.createElement(Btn, {
|
|
1095
|
+
primary: true,
|
|
1096
|
+
disabled: !localSel.length,
|
|
1097
|
+
onClick: function () { transfer("upload", localSel); },
|
|
1098
|
+
}, t("upload")),
|
|
1099
|
+
React.createElement(Btn, {
|
|
1100
|
+
primary: true,
|
|
1101
|
+
disabled: !remoteSel.length,
|
|
1102
|
+
onClick: function () { transfer("download", remoteSel); },
|
|
1103
|
+
}, t("download")),
|
|
1104
|
+
React.createElement(Btn, { onClick: onClose }, t("close")),
|
|
1105
|
+
),
|
|
1106
|
+
React.createElement("div", { className: "ssh-ov-body ssh-sftp" },
|
|
1107
|
+
React.createElement("div", { className: "ssh-sftp-panes" },
|
|
1108
|
+
pane("local"),
|
|
1109
|
+
pane("remote"),
|
|
1110
|
+
),
|
|
1111
|
+
React.createElement("div", { className: "ssh-status" },
|
|
1112
|
+
(status || t("sftpHint")) + (err ? " · " + err : ""),
|
|
1113
|
+
),
|
|
1114
|
+
),
|
|
1115
|
+
ctx ? React.createElement("div", {
|
|
1116
|
+
className: "ssh-ctx",
|
|
1117
|
+
style: { left: ctx.x, top: ctx.y },
|
|
1118
|
+
onMouseDown: function (e) { e.stopPropagation(); },
|
|
1119
|
+
},
|
|
1120
|
+
ctx.entry ? React.createElement("button", { type: "button", onClick: function () { ctxAction("open"); } }, t("open")) : null,
|
|
1121
|
+
ctx.side === "local" ? React.createElement("button", { type: "button", onClick: function () { ctxAction("upload"); } }, t("uploadToRemote")) : null,
|
|
1122
|
+
ctx.side === "remote" ? React.createElement("button", { type: "button", onClick: function () { ctxAction("download"); } }, t("downloadToLocal")) : null,
|
|
1123
|
+
React.createElement("button", { type: "button", onClick: function () { ctxAction("mkdir"); } }, t("newFolder")),
|
|
1124
|
+
ctx.entry ? React.createElement("button", { type: "button", onClick: function () { ctxAction("rename"); } }, t("rename")) : null,
|
|
1125
|
+
ctx.entry ? React.createElement("button", { type: "button", onClick: function () { ctxAction("copyPath"); } }, t("copyPath")) : null,
|
|
1126
|
+
React.createElement("hr"),
|
|
1127
|
+
React.createElement("button", { type: "button", className: "danger", onClick: function () { ctxAction("delete"); } }, t("delete")),
|
|
1128
|
+
) : null,
|
|
1129
|
+
ctx ? React.createElement("div", {
|
|
1130
|
+
style: { position: "fixed", inset: 0, zIndex: 12999 },
|
|
1131
|
+
onMouseDown: function () { setCtx(null); },
|
|
1132
|
+
onContextMenu: function (e) { e.preventDefault(); setCtx(null); },
|
|
1133
|
+
}) : null,
|
|
1134
|
+
React.createElement(TextDialog, {
|
|
1135
|
+
open: !!dialog,
|
|
1136
|
+
token: dialog && dialog.token,
|
|
1137
|
+
mode: dialog && dialog.mode,
|
|
1138
|
+
title: dialog && dialog.title,
|
|
1139
|
+
description: dialog && dialog.description,
|
|
1140
|
+
defaultValue: dialog && dialog.defaultValue,
|
|
1141
|
+
placeholder: dialog && dialog.placeholder,
|
|
1142
|
+
confirmLabel: dialog && dialog.confirmLabel,
|
|
1143
|
+
danger: dialog && dialog.danger,
|
|
1144
|
+
onCancel: function () { finishDialog(null); },
|
|
1145
|
+
onConfirm: function (v) { finishDialog(v); },
|
|
1146
|
+
}),
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
function TunnelPanel(props) {
|
|
1151
|
+
const visible = props.visible;
|
|
1152
|
+
const scope = props.scope || {};
|
|
1153
|
+
const [view, setView] = React.useState("tunnel");
|
|
1154
|
+
const [projectPathKey, setProjectPathKey] = React.useState("");
|
|
1155
|
+
const [sessionId, setSessionId] = React.useState(scope.sessionId || "");
|
|
1156
|
+
const [hosts, setHosts] = React.useState([]);
|
|
1157
|
+
const [granted, setGranted] = React.useState([]);
|
|
1158
|
+
const [sessions, setSessions] = React.useState([]);
|
|
1159
|
+
const [prompts, setPrompts] = React.useState([]);
|
|
1160
|
+
const [scan, setScan] = React.useState(null);
|
|
1161
|
+
const [form, setForm] = React.useState(emptyForm());
|
|
1162
|
+
const [sftpOn, setSftpOn] = React.useState(true);
|
|
1163
|
+
const [busy, setBusy] = React.useState("");
|
|
1164
|
+
const [message, setMessage] = React.useState("");
|
|
1165
|
+
const [error, setError] = React.useState("");
|
|
1166
|
+
// center overlay state
|
|
1167
|
+
const [overlay, setOverlay] = React.useState(null); // {sessionId, title, mode:'bash'|'sftp'}
|
|
1168
|
+
// in-panel dialog (same machinery as CenterSftp) — replaces window.confirm
|
|
1169
|
+
const [dialog, setDialog] = React.useState(null);
|
|
1170
|
+
const dialogToken = React.useRef(0);
|
|
1171
|
+
function openDialog(cfg) {
|
|
1172
|
+
dialogToken.current += 1;
|
|
1173
|
+
return new Promise(function (resolve) {
|
|
1174
|
+
setDialog(Object.assign({}, cfg, { token: dialogToken.current, _resolve: resolve }));
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
function finishDialog(result) {
|
|
1178
|
+
setDialog(function (cur) {
|
|
1179
|
+
if (cur && cur._resolve) cur._resolve(result);
|
|
1180
|
+
return null;
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
async function askConfirm(opts) {
|
|
1184
|
+
const v = await openDialog({
|
|
1185
|
+
mode: "confirm",
|
|
1186
|
+
title: opts.title,
|
|
1187
|
+
description: opts.description || "",
|
|
1188
|
+
confirmLabel: opts.confirmLabel || t("ok"),
|
|
1189
|
+
danger: !!opts.danger,
|
|
1190
|
+
});
|
|
1191
|
+
return !!v;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// scope.cwd is the real scope key better-sidebar passes; repoRoot is
|
|
1195
|
+
// the accepted alias. workspacePath was never provided by any host.
|
|
1196
|
+
const cwdGuess = scope.cwd || scope.repoRoot || "";
|
|
1197
|
+
|
|
1198
|
+
const refresh = React.useCallback(async () => {
|
|
1199
|
+
setError("");
|
|
1200
|
+
const ctx = await api("getProjectContext", {
|
|
1201
|
+
sessionId: scope.sessionId || sessionId || "",
|
|
1202
|
+
cwd: cwdGuess || undefined,
|
|
1203
|
+
});
|
|
1204
|
+
const key = ctx.projectPathKey || cwdGuess || "";
|
|
1205
|
+
setProjectPathKey(key);
|
|
1206
|
+
if (ctx.sessionId) setSessionId(ctx.sessionId);
|
|
1207
|
+
const [h, g, s, p] = await Promise.all([
|
|
1208
|
+
api("listHosts"),
|
|
1209
|
+
api("getGrants", { projectPathKey: key }),
|
|
1210
|
+
api("listSessions", { projectPathKey: key }),
|
|
1211
|
+
api("listPrompts", { projectPathKey: key }),
|
|
1212
|
+
]);
|
|
1213
|
+
setHosts(h.hosts || []);
|
|
1214
|
+
setGranted(g.hostIds || []);
|
|
1215
|
+
setSessions(s.sessions || []);
|
|
1216
|
+
setPrompts(p.prompts || []);
|
|
1217
|
+
}, [scope.sessionId, cwdGuess, sessionId]);
|
|
1218
|
+
|
|
1219
|
+
React.useEffect(() => {
|
|
1220
|
+
if (!visible) return;
|
|
1221
|
+
refresh().catch(function (e) { setError(String(e && e.message ? e.message : e)); });
|
|
1222
|
+
const t = setInterval(function () {
|
|
1223
|
+
if (!visible) return;
|
|
1224
|
+
// Background polls log instead of swallowing: a broken host route
|
|
1225
|
+
// should be diagnosable from the console (C-23) while the UI
|
|
1226
|
+
// keeps the last known state.
|
|
1227
|
+
api("listSessions", { projectPathKey: projectPathKey || cwdGuess })
|
|
1228
|
+
.then(function (s) { setSessions(s.sessions || []); })
|
|
1229
|
+
.catch(function (e) { console.warn("[dsh-ssh-tunnel] session poll failed:", e && e.message ? e.message : e); });
|
|
1230
|
+
api("listPrompts", { projectPathKey: projectPathKey || cwdGuess })
|
|
1231
|
+
.then(function (p) { setPrompts(p.prompts || []); })
|
|
1232
|
+
.catch(function (e) { console.warn("[dsh-ssh-tunnel] prompt poll failed:", e && e.message ? e.message : e); });
|
|
1233
|
+
}, 4000);
|
|
1234
|
+
return function () { clearInterval(t); };
|
|
1235
|
+
}, [visible, refresh, projectPathKey, cwdGuess]);
|
|
1236
|
+
|
|
1237
|
+
function patchForm(p) {
|
|
1238
|
+
setForm(function (prev) { return Object.assign({}, prev, p); });
|
|
1239
|
+
}
|
|
1240
|
+
async function run(name, fn) {
|
|
1241
|
+
setBusy(name); setMessage(""); setError("");
|
|
1242
|
+
try { await fn(); setMessage(""); await refresh(); }
|
|
1243
|
+
catch (e) { setError(String(e && e.message ? e.message : e)); }
|
|
1244
|
+
finally { setBusy(""); }
|
|
1245
|
+
}
|
|
1246
|
+
function toggleGrant(id) {
|
|
1247
|
+
setGranted(granted.includes(id) ? granted.filter(function (x) { return x !== id; }) : granted.concat([id]));
|
|
1248
|
+
}
|
|
1249
|
+
function openCenter(sess, mode) {
|
|
1250
|
+
setOverlay({
|
|
1251
|
+
sessionId: sess.session_id,
|
|
1252
|
+
title: sess.title || sess.endpoint || sess.session_id,
|
|
1253
|
+
mode: mode,
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
const tabs = [
|
|
1258
|
+
{ id: "grants", label: t("tabGrants") },
|
|
1259
|
+
{ id: "hosts", label: t("tabHosts") },
|
|
1260
|
+
{ id: "tunnel", label: t("tabTunnel") },
|
|
1261
|
+
];
|
|
1262
|
+
|
|
1263
|
+
const overlayEl = overlay
|
|
1264
|
+
? (overlay.mode === "bash"
|
|
1265
|
+
? React.createElement(CenterBash, {
|
|
1266
|
+
sessionId: overlay.sessionId,
|
|
1267
|
+
projectPathKey: projectPathKey,
|
|
1268
|
+
title: overlay.title,
|
|
1269
|
+
onClose: function () { setOverlay(null); },
|
|
1270
|
+
onOpenSftp: function () { setOverlay(Object.assign({}, overlay, { mode: "sftp" })); },
|
|
1271
|
+
})
|
|
1272
|
+
: React.createElement(CenterSftp, {
|
|
1273
|
+
sessionId: overlay.sessionId,
|
|
1274
|
+
projectPathKey: projectPathKey,
|
|
1275
|
+
title: overlay.title,
|
|
1276
|
+
onClose: function () { setOverlay(null); },
|
|
1277
|
+
onOpenBash: function () { setOverlay(Object.assign({}, overlay, { mode: "bash" })); },
|
|
1278
|
+
}))
|
|
1279
|
+
: null;
|
|
1280
|
+
|
|
1281
|
+
return React.createElement(React.Fragment, null,
|
|
1282
|
+
overlayEl,
|
|
1283
|
+
React.createElement("div", { className: "ssh-t-root" },
|
|
1284
|
+
React.createElement("div", { className: "ssh-t-head" },
|
|
1285
|
+
React.createElement("div", null,
|
|
1286
|
+
React.createElement("div", { className: "ssh-t-title" }, t("tabTitle")),
|
|
1287
|
+
React.createElement("div", { className: "ssh-t-sub" }, t("projectLabel", { path: projectPathKey || t("projectUnbound") })),
|
|
1288
|
+
),
|
|
1289
|
+
React.createElement(Btn, { disabled: !!busy, onClick: function () {
|
|
1290
|
+
refresh().catch(function (e) { setError(String(e.message || e)); });
|
|
1291
|
+
} }, t("refresh")),
|
|
1292
|
+
),
|
|
1293
|
+
React.createElement("div", { className: "ssh-t-tabs" },
|
|
1294
|
+
tabs.map(function (t) {
|
|
1295
|
+
return React.createElement("button", {
|
|
1296
|
+
key: t.id, type: "button",
|
|
1297
|
+
className: "ssh-t-tab" + (view === t.id || (view === "edit" && t.id === "hosts") ? " on" : ""),
|
|
1298
|
+
onClick: function () { setView(t.id); },
|
|
1299
|
+
}, t.label);
|
|
1300
|
+
}),
|
|
1301
|
+
),
|
|
1302
|
+
prompts && prompts.length ? React.createElement("div", { className: "ssh-t-prompt" },
|
|
1303
|
+
React.createElement("div", { style: { fontWeight: 600, marginBottom: 6 } }, t("needConfirm")),
|
|
1304
|
+
prompts.map(function (p) {
|
|
1305
|
+
const changed = p.kind === "host_key_changed";
|
|
1306
|
+
const newFp = p.fingerprint
|
|
1307
|
+
? ((p.fingerprintAlgo || "SHA256") + ":" + String(p.fingerprint))
|
|
1308
|
+
: "";
|
|
1309
|
+
const monoStyle = {
|
|
1310
|
+
marginTop: 6, wordBreak: "break-all",
|
|
1311
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", fontSize: 11,
|
|
1312
|
+
};
|
|
1313
|
+
// Structured render: kind label + endpoint + old/new
|
|
1314
|
+
// fingerprints from fields. The host `message` string embeds
|
|
1315
|
+
// the new fingerprint too — showing both duplicated it.
|
|
1316
|
+
return React.createElement("div", { key: p.promptId, style: { marginBottom: 8 } },
|
|
1317
|
+
React.createElement("div", null,
|
|
1318
|
+
(changed ? t("promptKeyChanged") : t("promptKeyUnknown"))
|
|
1319
|
+
+ " · " + p.host + ":" + (p.port || 22)),
|
|
1320
|
+
newFp
|
|
1321
|
+
? React.createElement("div", { className: "ssh-t-muted", style: monoStyle }, t("promptFpNew") + ": " + newFp)
|
|
1322
|
+
: null,
|
|
1323
|
+
changed && p.previous
|
|
1324
|
+
? React.createElement("div", { className: "ssh-t-muted", style: monoStyle }, t("promptFpOld") + ": SHA256:" + String(p.previous))
|
|
1325
|
+
: null,
|
|
1326
|
+
React.createElement("div", { className: "ssh-t-actions" },
|
|
1327
|
+
React.createElement(Btn, { primary: true, disabled: !!busy, onClick: function () {
|
|
1328
|
+
run("trust", async function () {
|
|
1329
|
+
const r = await api("answerPrompt", {
|
|
1330
|
+
promptId: p.promptId, trustHostKey: true, connectAfter: true,
|
|
1331
|
+
projectPathKey: projectPathKey, sftpEnabled: sftpOn,
|
|
1332
|
+
});
|
|
1333
|
+
// C-08: "trusted but not connected" must not look
|
|
1334
|
+
// like a failed trust — say both parts explicitly.
|
|
1335
|
+
if (r && r.trusted && r.connected === false && r.error) {
|
|
1336
|
+
throw new Error(t("trustedNotConnected") + " " + String(r.error));
|
|
1337
|
+
}
|
|
1338
|
+
if (r && r.connected === false && r.error) throw new Error(String(r.error));
|
|
1339
|
+
});
|
|
1340
|
+
} }, t("trustConnect")),
|
|
1341
|
+
React.createElement(Btn, { disabled: !!busy, onClick: function () {
|
|
1342
|
+
run("dismiss", async function () {
|
|
1343
|
+
// No trustHostKey: the host drops the prompt and
|
|
1344
|
+
// stores nothing.
|
|
1345
|
+
await api("answerPrompt", { promptId: p.promptId });
|
|
1346
|
+
});
|
|
1347
|
+
} }, t("dismissPrompt")),
|
|
1348
|
+
),
|
|
1349
|
+
);
|
|
1350
|
+
}),
|
|
1351
|
+
) : null,
|
|
1352
|
+
view === "grants" ? React.createElement("div", { className: "ssh-t-card" },
|
|
1353
|
+
React.createElement("div", { className: "ssh-t-muted", style: { marginBottom: 8 } }, t("grantsHint")),
|
|
1354
|
+
hosts.map(function (h) {
|
|
1355
|
+
return React.createElement("label", { key: h.id, className: "ssh-t-check" },
|
|
1356
|
+
React.createElement("input", {
|
|
1357
|
+
type: "checkbox", checked: granted.includes(h.id),
|
|
1358
|
+
onChange: function () { toggleGrant(h.id); },
|
|
1359
|
+
}),
|
|
1360
|
+
React.createElement("span", null,
|
|
1361
|
+
React.createElement("span", { className: "ssh-t-name" }, h.name || h.id),
|
|
1362
|
+
React.createElement("span", { className: "ssh-t-endpoint" },
|
|
1363
|
+
h.username + "@" + h.host + ":" + (h.port || 22) + " · " + h.credentialStatus,
|
|
1364
|
+
),
|
|
1365
|
+
),
|
|
1366
|
+
);
|
|
1367
|
+
}),
|
|
1368
|
+
React.createElement("div", { className: "ssh-t-actions" },
|
|
1369
|
+
React.createElement(Btn, { primary: true, disabled: !!busy || !projectPathKey, onClick: function () {
|
|
1370
|
+
run("grants", async function () {
|
|
1371
|
+
await api("setGrants", { projectPathKey: projectPathKey, hostIds: granted });
|
|
1372
|
+
});
|
|
1373
|
+
} }, t("saveGrants")),
|
|
1374
|
+
),
|
|
1375
|
+
) : null,
|
|
1376
|
+
view === "hosts" ? React.createElement("div", { className: "ssh-t-card" },
|
|
1377
|
+
React.createElement("div", { className: "ssh-t-actions", style: { marginTop: 0 } },
|
|
1378
|
+
React.createElement(Btn, { primary: true, onClick: function () { setForm(emptyForm()); setView("edit"); } }, t("newHost")),
|
|
1379
|
+
React.createElement(Btn, { disabled: !!busy, onClick: function () {
|
|
1380
|
+
run("scan", async function () { setScan(await api("scanOpenSsh")); });
|
|
1381
|
+
} }, t("scanOpenSsh")),
|
|
1382
|
+
),
|
|
1383
|
+
hosts.map(function (h) {
|
|
1384
|
+
return React.createElement("div", { key: h.id, className: "ssh-t-row" },
|
|
1385
|
+
React.createElement("div", { className: "ssh-t-row-main" },
|
|
1386
|
+
React.createElement("div", { className: "ssh-t-name" }, h.name || h.id),
|
|
1387
|
+
React.createElement("div", { className: "ssh-t-endpoint", title: h.username + "@" + h.host + ":" + h.port },
|
|
1388
|
+
h.username + "@" + h.host + ":" + h.port,
|
|
1389
|
+
),
|
|
1390
|
+
),
|
|
1391
|
+
React.createElement("div", { className: "ssh-t-actions" },
|
|
1392
|
+
React.createElement(Btn, { onClick: function () {
|
|
1393
|
+
setForm({
|
|
1394
|
+
id: h.id, name: h.name || "", host: h.host || "", port: h.port || 22,
|
|
1395
|
+
username: h.username || "",
|
|
1396
|
+
// C-19: a legacy keyboard-interactive host must not look
|
|
1397
|
+
// like an ordinary password host. Keep the retirement
|
|
1398
|
+
// visible with an explicit banner on the edit form.
|
|
1399
|
+
authType: h.authType === "keyboardInteractive" ? "password" : (h.authType || "privateKey"),
|
|
1400
|
+
legacyKbi: h.authType === "keyboardInteractive",
|
|
1401
|
+
privateKeyPath: h.privateKeyPath || "", password: "", privateKeyPem: "", passphrase: "",
|
|
1402
|
+
});
|
|
1403
|
+
setView("edit");
|
|
1404
|
+
} }, t("edit")),
|
|
1405
|
+
React.createElement(Btn, { danger: true, onClick: async function () {
|
|
1406
|
+
const ok = await askConfirm({
|
|
1407
|
+
title: t("confirmDeleteTitle"),
|
|
1408
|
+
description: t("deleteHostConfirm", { name: h.name || h.id }),
|
|
1409
|
+
confirmLabel: t("delete"),
|
|
1410
|
+
danger: true,
|
|
1411
|
+
});
|
|
1412
|
+
if (!ok) return;
|
|
1413
|
+
run("del", async function () { await api("deleteHost", { id: h.id }); });
|
|
1414
|
+
} }, t("delete")),
|
|
1415
|
+
),
|
|
1416
|
+
);
|
|
1417
|
+
}),
|
|
1418
|
+
scan ? React.createElement("div", { style: { marginTop: 12 } },
|
|
1419
|
+
(scan.configs || []).map(function (c, i) {
|
|
1420
|
+
return React.createElement("div", { key: i, className: "ssh-t-row" },
|
|
1421
|
+
React.createElement("div", { className: "ssh-t-row-main" },
|
|
1422
|
+
React.createElement("div", { className: "ssh-t-name" }, c.name || c.host),
|
|
1423
|
+
React.createElement("div", { className: "ssh-t-endpoint", title: c.host },
|
|
1424
|
+
(c.username ? c.username + "@" : "") + c.host + ":" + (c.port || 22),
|
|
1425
|
+
),
|
|
1426
|
+
c.privateKeyPath
|
|
1427
|
+
? React.createElement("div", { className: "ssh-t-endpoint" }, t("privateKeyPath") + ": " + c.privateKeyPath)
|
|
1428
|
+
: null,
|
|
1429
|
+
),
|
|
1430
|
+
React.createElement("div", { className: "ssh-t-actions" },
|
|
1431
|
+
React.createElement(Btn, { onClick: function () {
|
|
1432
|
+
run("imp", async function () { await api("importScan", { entry: c }); });
|
|
1433
|
+
} }, t("import")),
|
|
1434
|
+
),
|
|
1435
|
+
);
|
|
1436
|
+
}),
|
|
1437
|
+
) : null,
|
|
1438
|
+
) : null,
|
|
1439
|
+
view === "edit" ? React.createElement("div", { className: "ssh-t-card" },
|
|
1440
|
+
React.createElement(Field, { label: t("name") }, React.createElement("input", { className: "ssh-t-input", value: form.name, onChange: function (e) { patchForm({ name: e.target.value }); } })),
|
|
1441
|
+
React.createElement(Field, { label: t("host") }, React.createElement("input", { className: "ssh-t-input", value: form.host, onChange: function (e) { patchForm({ host: e.target.value }); } })),
|
|
1442
|
+
React.createElement(Field, { label: t("port") }, React.createElement("input", { className: "ssh-t-input", type: "number", value: form.port, onChange: function (e) { patchForm({ port: e.target.value }); } })),
|
|
1443
|
+
React.createElement(Field, { label: t("username") }, React.createElement("input", { className: "ssh-t-input", value: form.username, onChange: function (e) { patchForm({ username: e.target.value }); } })),
|
|
1444
|
+
React.createElement(Field, { label: t("authType") },
|
|
1445
|
+
React.createElement("select", { className: "ssh-t-select", value: form.authType, onChange: function (e) { patchForm({ authType: e.target.value, legacyKbi: false }); } },
|
|
1446
|
+
React.createElement("option", { value: "privateKey" }, t("authPrivateKey")),
|
|
1447
|
+
React.createElement("option", { value: "password" }, t("authPassword")),
|
|
1448
|
+
),
|
|
1449
|
+
),
|
|
1450
|
+
form.authType === "privateKey" ? React.createElement(React.Fragment, null,
|
|
1451
|
+
React.createElement(Field, { label: t("privateKeyPath") }, React.createElement("input", { className: "ssh-t-input", value: form.privateKeyPath, onChange: function (e) { patchForm({ privateKeyPath: e.target.value }); } })),
|
|
1452
|
+
React.createElement(Field, { label: t("privateKeyPem") }, React.createElement("textarea", { className: "ssh-t-textarea", rows: 3, value: form.privateKeyPem, onChange: function (e) { patchForm({ privateKeyPem: e.target.value }); } })),
|
|
1453
|
+
React.createElement(Field, { label: t("passphrase") }, React.createElement("input", { className: "ssh-t-input", type: "password", value: form.passphrase, onChange: function (e) { patchForm({ passphrase: e.target.value }); } })),
|
|
1454
|
+
) : null,
|
|
1455
|
+
form.authType === "password" ? React.createElement(Field, { label: t("password") }, React.createElement("input", { className: "ssh-t-input", type: "password", value: form.password, onChange: function (e) { patchForm({ password: e.target.value }); } })) : null,
|
|
1456
|
+
form.legacyKbi ? React.createElement("div", { className: "ssh-t-err" }, t("kbiEditNotice")) : null,
|
|
1457
|
+
React.createElement("div", { className: "ssh-t-actions" },
|
|
1458
|
+
React.createElement(Btn, { primary: true, disabled: !!busy, onClick: function () {
|
|
1459
|
+
run("save", async function () {
|
|
1460
|
+
const payload = {
|
|
1461
|
+
id: form.id || undefined, name: form.name, host: form.host,
|
|
1462
|
+
port: Number(form.port) || 22, username: form.username, authType: form.authType,
|
|
1463
|
+
privateKeyPath: form.privateKeyPath,
|
|
1464
|
+
};
|
|
1465
|
+
if (form.password) payload.password = form.password;
|
|
1466
|
+
if (form.privateKeyPem) payload.privateKeyPem = form.privateKeyPem;
|
|
1467
|
+
if (form.passphrase) payload.passphrase = form.passphrase;
|
|
1468
|
+
await api("saveHost", { host: payload });
|
|
1469
|
+
setView("hosts");
|
|
1470
|
+
});
|
|
1471
|
+
} }, t("save")),
|
|
1472
|
+
React.createElement(Btn, { onClick: function () { setView("hosts"); } }, t("back")),
|
|
1473
|
+
),
|
|
1474
|
+
) : null,
|
|
1475
|
+
view === "tunnel" ? React.createElement("div", { className: "ssh-t-card" },
|
|
1476
|
+
React.createElement("label", { className: "ssh-t-check" },
|
|
1477
|
+
React.createElement("input", { type: "checkbox", checked: sftpOn, onChange: function (e) { setSftpOn(!!e.target.checked); } }),
|
|
1478
|
+
React.createElement("span", null, t("sftpOnConnect")),
|
|
1479
|
+
),
|
|
1480
|
+
React.createElement("div", { style: { fontWeight: 600, margin: "8px 0" } }, t("connectSection")),
|
|
1481
|
+
hosts.filter(function (h) { return granted.includes(h.id); }).map(function (h) {
|
|
1482
|
+
return React.createElement("div", { key: h.id, className: "ssh-t-row" },
|
|
1483
|
+
React.createElement("div", { className: "ssh-t-row-main" },
|
|
1484
|
+
React.createElement("div", { className: "ssh-t-name" }, h.name || h.id),
|
|
1485
|
+
React.createElement("div", { className: "ssh-t-endpoint", title: h.username + "@" + h.host + ":" + (h.port || 22) },
|
|
1486
|
+
h.username + "@" + h.host + ":" + (h.port || 22),
|
|
1487
|
+
),
|
|
1488
|
+
),
|
|
1489
|
+
React.createElement("div", { className: "ssh-t-actions" },
|
|
1490
|
+
React.createElement(Btn, { primary: true, disabled: !!busy || !projectPathKey, onClick: function () {
|
|
1491
|
+
run("connect", async function () {
|
|
1492
|
+
if (h.authType === "keyboardInteractive") {
|
|
1493
|
+
throw new Error(t("kbiUnsupported"));
|
|
1494
|
+
}
|
|
1495
|
+
try {
|
|
1496
|
+
await api("connect", {
|
|
1497
|
+
hostId: h.id, projectPathKey: projectPathKey,
|
|
1498
|
+
sftpEnabled: sftpOn, trustHostKey: false,
|
|
1499
|
+
});
|
|
1500
|
+
} catch (e) {
|
|
1501
|
+
// A host-key prompt arrives as ok:false + promptId
|
|
1502
|
+
// (HTTP 200): surface it and refresh the card. The
|
|
1503
|
+
// fallback copy covers hosts that omit `message`.
|
|
1504
|
+
if (e && e.data && e.data.promptId) {
|
|
1505
|
+
setMessage(e.data.message || t("needHostKey"));
|
|
1506
|
+
setPrompts((await api("listPrompts", { projectPathKey: projectPathKey })).prompts || []);
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
throw e;
|
|
1510
|
+
}
|
|
1511
|
+
});
|
|
1512
|
+
} }, t("connect")),
|
|
1513
|
+
),
|
|
1514
|
+
);
|
|
1515
|
+
}),
|
|
1516
|
+
React.createElement("div", { style: { fontWeight: 600, margin: "12px 0 6px" } }, t("sessionsSection")),
|
|
1517
|
+
sessions.length === 0
|
|
1518
|
+
? React.createElement("div", { className: "ssh-t-muted" }, t("noSessions"))
|
|
1519
|
+
: sessions.map(function (s) {
|
|
1520
|
+
const live = s.running && s.status === "connected";
|
|
1521
|
+
const reconnecting = s.status === "reconnecting";
|
|
1522
|
+
const statusLabel = reconnecting
|
|
1523
|
+
? t("reconnecting", { attempt: s.reconnect_attempt || 1, max: s.reconnect_max_attempts || 3 })
|
|
1524
|
+
: (live ? t("connected") : t("disconnected"));
|
|
1525
|
+
return React.createElement("div", { key: s.session_id, className: "ssh-t-row" },
|
|
1526
|
+
React.createElement("div", { className: "ssh-t-row-main" },
|
|
1527
|
+
React.createElement("div", { className: "ssh-t-name" }, s.title || s.session_id),
|
|
1528
|
+
React.createElement("div", { className: "ssh-t-endpoint" },
|
|
1529
|
+
(s.endpoint ? s.endpoint + " · " : "") + statusLabel + " · sftp=" + String(s.sftpEnabled),
|
|
1530
|
+
),
|
|
1531
|
+
),
|
|
1532
|
+
React.createElement("div", { className: "ssh-t-actions" },
|
|
1533
|
+
React.createElement(Btn, { primary: true, disabled: !live, onClick: function () { openCenter(s, "bash"); } }, t("terminal")),
|
|
1534
|
+
React.createElement(Btn, { disabled: !live || !s.sftpEnabled, onClick: function () { openCenter(s, "sftp"); } }, t("sftp")),
|
|
1535
|
+
!live && !reconnecting ? React.createElement(Btn, { primary: true, disabled: !!busy || !projectPathKey, onClick: function () {
|
|
1536
|
+
run("reconnect", async function () {
|
|
1537
|
+
try {
|
|
1538
|
+
await api("reconnect", { sessionId: s.session_id, projectPathKey: projectPathKey });
|
|
1539
|
+
} catch (e) {
|
|
1540
|
+
const msg = String(e && e.message ? e.message : e);
|
|
1541
|
+
if (e && e.data && e.data.promptId) {
|
|
1542
|
+
setMessage(e.data.message || t("needHostKey"));
|
|
1543
|
+
setPrompts((await api("listPrompts", { projectPathKey: projectPathKey })).prompts || []);
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
if (/keyboard-interactive/i.test(msg)) throw new Error(t("kbiUnsupported"));
|
|
1547
|
+
throw e;
|
|
1548
|
+
}
|
|
1549
|
+
});
|
|
1550
|
+
} }, t("reconnect")) : null,
|
|
1551
|
+
React.createElement(Btn, { danger: true, onClick: function () {
|
|
1552
|
+
run("disc", async function () {
|
|
1553
|
+
if (overlay && overlay.sessionId === s.session_id) setOverlay(null);
|
|
1554
|
+
await api("disconnect", { sessionId: s.session_id, projectPathKey: projectPathKey });
|
|
1555
|
+
});
|
|
1556
|
+
} }, live ? t("disconnect") : t("closeSession")),
|
|
1557
|
+
),
|
|
1558
|
+
);
|
|
1559
|
+
}),
|
|
1560
|
+
) : null,
|
|
1561
|
+
message ? React.createElement("div", { className: "ssh-t-ok" }, message) : null,
|
|
1562
|
+
error ? React.createElement("div", { className: "ssh-t-err" }, error) : null,
|
|
1563
|
+
React.createElement("div", { className: "ssh-t-muted" },
|
|
1564
|
+
t("footerHint"),
|
|
1565
|
+
),
|
|
1566
|
+
React.createElement(TextDialog, {
|
|
1567
|
+
open: !!dialog,
|
|
1568
|
+
token: dialog && dialog.token,
|
|
1569
|
+
mode: dialog && dialog.mode,
|
|
1570
|
+
title: dialog && dialog.title,
|
|
1571
|
+
description: dialog && dialog.description,
|
|
1572
|
+
confirmLabel: dialog && dialog.confirmLabel,
|
|
1573
|
+
danger: dialog && dialog.danger,
|
|
1574
|
+
onCancel: function () { finishDialog(null); },
|
|
1575
|
+
onConfirm: function (v) { finishDialog(v); },
|
|
1576
|
+
}),
|
|
1577
|
+
),
|
|
1578
|
+
);
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
function TunnelRoot(props) {
|
|
1582
|
+
const ctx = props.ctx;
|
|
1583
|
+
// Re-render on DSH locale switches (better-sidebar pattern).
|
|
1584
|
+
const localeKey = React.useSyncExternalStore(
|
|
1585
|
+
React.useCallback(function (cb) {
|
|
1586
|
+
if (!ctx.locale || typeof ctx.locale.subscribe !== "function") return function () {};
|
|
1587
|
+
return ctx.locale.subscribe(cb);
|
|
1588
|
+
}, [ctx]),
|
|
1589
|
+
React.useCallback(function () {
|
|
1590
|
+
try {
|
|
1591
|
+
return ctx.locale && ctx.locale.getSnapshot ? ctx.locale.getSnapshot().active : activeLocale();
|
|
1592
|
+
} catch (e) {
|
|
1593
|
+
return activeLocale();
|
|
1594
|
+
}
|
|
1595
|
+
}, [ctx]),
|
|
1596
|
+
function () { return "en"; },
|
|
1597
|
+
);
|
|
1598
|
+
return React.createElement(TunnelPanel, {
|
|
1599
|
+
key: "ssh-tunnel-" + String(localeKey || "en"),
|
|
1600
|
+
visible: props.visible,
|
|
1601
|
+
scope: props.scope,
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
const inject = ["betterSidebar", "locale"];
|
|
1606
|
+
function apply(ctx) {
|
|
1607
|
+
ctx.effect(function () {
|
|
1608
|
+
return ensureStyles();
|
|
1609
|
+
}, "dsh-ssh-tunnel: styles");
|
|
1610
|
+
if (ctx.locale) {
|
|
1611
|
+
attachLocale(ctx.locale);
|
|
1612
|
+
ctx.effect(function () {
|
|
1613
|
+
const offZh = ctx.locale.register(LOCALE_NS, "zh", zh);
|
|
1614
|
+
const offEn = ctx.locale.register(LOCALE_NS, "en", en);
|
|
1615
|
+
return function () {
|
|
1616
|
+
try { offZh(); } catch (e) {}
|
|
1617
|
+
try { offEn(); } catch (e) {}
|
|
1618
|
+
};
|
|
1619
|
+
}, "dsh-ssh-tunnel: dictionaries");
|
|
1620
|
+
}
|
|
1621
|
+
if (!ctx.betterSidebar) return;
|
|
1622
|
+
ctx.effect(function () {
|
|
1623
|
+
return ctx.betterSidebar.registerTab({
|
|
1624
|
+
id: TAB_ID,
|
|
1625
|
+
title: function () { return t("tabTitle"); },
|
|
1626
|
+
icon: function (size) { return icon(size); },
|
|
1627
|
+
order: 44,
|
|
1628
|
+
single: true,
|
|
1629
|
+
component: function (p) {
|
|
1630
|
+
return React.createElement(TunnelRoot, {
|
|
1631
|
+
ctx: ctx,
|
|
1632
|
+
visible: p.visible,
|
|
1633
|
+
scope: p.scope,
|
|
1634
|
+
});
|
|
1635
|
+
},
|
|
1636
|
+
});
|
|
1637
|
+
}, "dsh-ssh-tunnel: register tab");
|
|
1638
|
+
}
|
|
1639
|
+
exports.apply = apply;
|
|
1640
|
+
exports.inject = inject;
|
|
1641
|
+
return module.exports;
|
|
1642
|
+
},
|
|
1643
|
+
});
|