@linxin666/dsh-ssh 0.3.6 → 0.3.10

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/lib/client.js CHANGED
@@ -24,302 +24,6 @@ window.__ModuleLoader__.load({
24
24
  terminal: "/api/dsh-ssh/terminal"
25
25
  };
26
26
  //#endregion
27
- //#region src/client/api.ts
28
- /**
29
- * Browser-side API client for the /api/dsh-ssh route family. The only data
30
- * access path the panel components use — plain fetch/WebSocket, same origin.
31
- */
32
- /** Error carrying the route's JSON error message. */
33
- var SshApiError = class extends Error {
34
- constructor(message) {
35
- super(message);
36
- this.name = "SshApiError";
37
- }
38
- };
39
- /** Extract the `.error` message from a JSON error body; undefined when absent. */
40
- function extractError(text) {
41
- if (text === "") return void 0;
42
- try {
43
- const parsed = JSON.parse(text);
44
- return typeof parsed?.error === "string" ? parsed.error : void 0;
45
- } catch {
46
- return;
47
- }
48
- }
49
- /** Parse a JSON response or throw an SshApiError. */
50
- async function readJson(response) {
51
- let body;
52
- try {
53
- body = await response.json();
54
- } catch {
55
- throw new SshApiError(`HTTP ${response.status}: invalid JSON response`);
56
- }
57
- if (!response.ok) throw new SshApiError(typeof body === "object" && body !== null && typeof body.error === "string" ? body.error : `HTTP ${response.status}`);
58
- return body;
59
- }
60
- /** Query-string helper. */
61
- function query(params) {
62
- const search = new URLSearchParams();
63
- for (const [key, value] of Object.entries(params)) if (value !== void 0 && value !== "") search.set(key, String(value));
64
- const text = search.toString();
65
- return text === "" ? "" : "?" + text;
66
- }
67
- /** The browser half's only data entry point. */
68
- var SshApi = class {
69
- async listHosts(queryText) {
70
- return (await readJson(await fetch(SSH_API.hosts + query({ query: queryText })))).hosts;
71
- }
72
- async createHost(payload) {
73
- return (await readJson(await fetch(SSH_API.hosts, {
74
- method: "POST",
75
- headers: { "content-type": "application/json" },
76
- body: JSON.stringify(payload)
77
- }))).host;
78
- }
79
- async updateHost(alias, patch) {
80
- return (await readJson(await fetch(SSH_API.hosts + query({ alias }), {
81
- method: "PATCH",
82
- headers: { "content-type": "application/json" },
83
- body: JSON.stringify(patch)
84
- }))).host;
85
- }
86
- async deleteHost(alias) {
87
- await readJson(await fetch(SSH_API.hosts + query({ alias }), { method: "DELETE" }));
88
- }
89
- async importSshConfig() {
90
- return (await readJson(await fetch(SSH_API.importSshConfig, { method: "POST" }))).result;
91
- }
92
- async testHost(alias) {
93
- return (await readJson(await fetch(SSH_API.test, {
94
- method: "POST",
95
- headers: { "content-type": "application/json" },
96
- body: JSON.stringify({ alias })
97
- }))).result;
98
- }
99
- async exec(alias, command, timeoutMs) {
100
- return (await readJson(await fetch(SSH_API.exec, {
101
- method: "POST",
102
- headers: { "content-type": "application/json" },
103
- body: JSON.stringify({
104
- alias,
105
- command,
106
- timeoutMs
107
- })
108
- }))).result;
109
- }
110
- async cluster(options) {
111
- return (await readJson(await fetch(SSH_API.cluster, {
112
- method: "POST",
113
- headers: { "content-type": "application/json" },
114
- body: JSON.stringify(options)
115
- }))).results;
116
- }
117
- async ls(alias, path) {
118
- return (await readJson(await fetch(SSH_API.ls + query({
119
- alias,
120
- path
121
- })))).entries;
122
- }
123
- async listTunnels() {
124
- return (await readJson(await fetch(SSH_API.tunnel, {
125
- method: "POST",
126
- headers: { "content-type": "application/json" },
127
- body: JSON.stringify({ action: "list" })
128
- }))).tunnels;
129
- }
130
- async startTunnel(options) {
131
- return (await readJson(await fetch(SSH_API.tunnel, {
132
- method: "POST",
133
- headers: { "content-type": "application/json" },
134
- body: JSON.stringify({
135
- action: "start",
136
- ...options
137
- })
138
- }))).tunnel;
139
- }
140
- async stopTunnel(tunnelId) {
141
- return (await readJson(await fetch(SSH_API.tunnel, {
142
- method: "POST",
143
- headers: { "content-type": "application/json" },
144
- body: JSON.stringify({
145
- action: "stop",
146
- tunnelId
147
- })
148
- }))).ok;
149
- }
150
- async stopAllTunnels(alias) {
151
- return (await readJson(await fetch(SSH_API.tunnel, {
152
- method: "POST",
153
- headers: { "content-type": "application/json" },
154
- body: JSON.stringify({
155
- action: "stop-all",
156
- alias
157
- })
158
- }))).stopped;
159
- }
160
- /**
161
- * Upload one file (raw bytes) to a remote path. Progress arrives through
162
- * the NDJSON response stream; resolves when the result frame lands.
163
- */
164
- async uploadFile(file, alias, remotePath, onProgress) {
165
- const response = await fetch(SSH_API.upload + query({
166
- alias,
167
- remotePath
168
- }), {
169
- method: "POST",
170
- body: file
171
- });
172
- if (!response.ok || response.body === null) throw new SshApiError(extractError(await response.text().catch(() => "")) ?? `upload failed: HTTP ${response.status}`);
173
- const reader = response.body.getReader();
174
- const decoder = new TextDecoder();
175
- let buffer = "";
176
- let finalError;
177
- let sawResult = false;
178
- let transferredBytes = 0;
179
- for (;;) {
180
- const { done, value } = await reader.read();
181
- if (done) break;
182
- buffer += decoder.decode(value, { stream: true });
183
- const lines = buffer.split("\n");
184
- buffer = lines.pop() ?? "";
185
- for (const line of lines) {
186
- if (line.trim() === "") continue;
187
- let parsed;
188
- try {
189
- parsed = JSON.parse(line);
190
- } catch {
191
- continue;
192
- }
193
- if (parsed.type === "progress") onProgress?.(parsed.progress);
194
- else if (parsed.type === "result") {
195
- sawResult = true;
196
- if (parsed.ok) transferredBytes = parsed.transferredBytes ?? 0;
197
- finalError = parsed.ok ? void 0 : parsed.error ?? "upload failed";
198
- }
199
- }
200
- }
201
- if (finalError !== void 0) throw new SshApiError(finalError);
202
- if (!sawResult) throw new SshApiError("upload ended without a result frame — the transfer did not complete");
203
- return { transferredBytes };
204
- }
205
- /**
206
- * Download a remote file with client-side progress. Streams straight to
207
- * disk when the File System Access API is available (no full-file RAM
208
- * copy); otherwise falls back to an in-memory Blob.
209
- */
210
- async downloadFile(alias, remotePath, onProgress) {
211
- const response = await fetch(SSH_API.download + query({
212
- alias,
213
- remotePath
214
- }));
215
- if (!response.ok || response.body === null) throw new SshApiError(extractError(await response.text().catch(() => "")) ?? `download failed: HTTP ${response.status}`);
216
- const total = Number(response.headers.get("content-length") ?? "0");
217
- const disposition = response.headers.get("content-disposition") ?? "";
218
- const filename = /filename="([^"]+)"/.exec(disposition)?.[1] ?? remotePath.split("/").pop() ?? "download";
219
- const reader = response.body.getReader();
220
- const picker = typeof window !== "undefined" ? window.showSaveFilePicker : void 0;
221
- let streamed = false;
222
- let writable;
223
- const chunks = [];
224
- let received = 0;
225
- const progress = () => {
226
- onProgress?.({
227
- phase: "transferring",
228
- file: remotePath,
229
- transferred: received,
230
- total,
231
- percent: total > 0 ? Math.round(received / total * 1e3) / 10 : 0
232
- });
233
- };
234
- try {
235
- if (picker !== void 0) {
236
- writable = await (await picker.call(window, { suggestedName: filename })).createWritable();
237
- streamed = true;
238
- }
239
- } catch {}
240
- for (;;) {
241
- const { done, value } = await reader.read();
242
- if (done) break;
243
- if (writable !== void 0) await writable.write(value);
244
- else chunks.push(value);
245
- received += value.length;
246
- progress();
247
- }
248
- if (writable !== void 0) await writable.close();
249
- onProgress?.({
250
- phase: "done",
251
- file: remotePath,
252
- transferred: received,
253
- total: received > 0 ? received : total,
254
- percent: 100
255
- });
256
- return {
257
- blob: streamed ? void 0 : new Blob(chunks),
258
- filename,
259
- streamed,
260
- bytes: received
261
- };
262
- }
263
- /** Open a WebSocket terminal session. */
264
- openTerminal(alias, cols, rows) {
265
- const url = (window.location.protocol === "https:" ? "wss" : "ws") + "://" + window.location.host + SSH_API.terminal + query({
266
- alias,
267
- cols,
268
- rows
269
- });
270
- const socket = new WebSocket(url);
271
- const connection = {
272
- onReady: void 0,
273
- onOutput: void 0,
274
- onExit: void 0,
275
- onAuthPrompt: void 0,
276
- send: (data) => {
277
- if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify({
278
- type: "input",
279
- data
280
- }));
281
- },
282
- resize: (cols, rows) => {
283
- if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify({
284
- type: "resize",
285
- cols,
286
- rows
287
- }));
288
- },
289
- sendAuthResponse: (responses) => {
290
- if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify({
291
- type: "auth_response",
292
- responses
293
- }));
294
- },
295
- close: () => {
296
- try {
297
- socket.close();
298
- } catch {}
299
- }
300
- };
301
- socket.onmessage = (event) => {
302
- let frame;
303
- try {
304
- frame = JSON.parse(event.data);
305
- } catch {
306
- return;
307
- }
308
- if (frame.type === "ready") connection.onReady?.();
309
- else if (frame.type === "output") connection.onOutput?.(frame.data);
310
- else if (frame.type === "exit") connection.onExit?.(frame.code, frame.error);
311
- else if (frame.type === "auth_prompt") connection.onAuthPrompt?.(frame.name, frame.instructions, frame.prompts);
312
- };
313
- socket.onclose = () => {
314
- connection.onExit?.(null, "connection closed");
315
- };
316
- socket.onerror = () => {
317
- connection.onExit?.(null, "connection error");
318
- };
319
- return connection;
320
- }
321
- };
322
- //#endregion
323
27
  //#region src/client/locales.ts
324
28
  /**
325
29
  * dsh-ssh surface copy: zh is the key source, en mirrors every key.
@@ -468,7 +172,8 @@ window.__ModuleLoader__.load({
468
172
  "common.loading": "加载中…",
469
173
  "common.error": "出错:{error}",
470
174
  "common.refresh": "刷新",
471
- "common.confirm": "确定"
175
+ "common.confirm": "确定",
176
+ "error.disabled": "SSH 插件在宿主端未启用。请前往「设置 → Web 插件 → SSH」打开 enabled 开关。"
472
177
  };
473
178
  const en = {
474
179
  "entry.label": "SSH",
@@ -614,7 +319,8 @@ window.__ModuleLoader__.load({
614
319
  "common.loading": "Loading…",
615
320
  "common.error": "Error: {error}",
616
321
  "common.refresh": "Refresh",
617
- "common.confirm": "Confirm"
322
+ "common.confirm": "Confirm",
323
+ "error.disabled": "The SSH plugin is disabled on the host. Please enable it in Settings → Web Plugins → SSH."
618
324
  };
619
325
  /** Tiny interpolation: {name} -> value. */
620
326
  function t(dictionary, key, values) {
@@ -633,8 +339,20 @@ window.__ModuleLoader__.load({
633
339
  function dictionary() {
634
340
  return (typeof document !== "undefined" ? document.documentElement.lang : "zh").toLowerCase().startsWith("en") ? { ...en } : { ...zh };
635
341
  }
342
+ /**
343
+ * SDK translate seat wired by the browser apply() once ctx.locale is bound
344
+ * (setRuntimeTranslate): reads the ACTIVE locale at call time, so plain-DOM
345
+ * surfaces follow a runtime language switch. The document-language pick stays
346
+ * as the unwired fallback (locale service absent, module-scope early callers).
347
+ */
348
+ let runtimeT;
349
+ /** Wire the SDK translate seat; pass undefined to restore the document-language pick. */
350
+ function setRuntimeTranslate(t) {
351
+ runtimeT = t;
352
+ }
636
353
  /** Translate a key with optional {name} template params (current language). */
637
354
  function tt(key, values) {
355
+ if (runtimeT !== void 0) return runtimeT(key, values);
638
356
  return t(dictionary(), key, values);
639
357
  }
640
358
  /** Human-readable error text from an unknown thrown value. */
@@ -670,8 +388,305 @@ window.__ModuleLoader__.load({
670
388
  if (value !== "") return value;
671
389
  }
672
390
  }
673
- return TERMINAL_FONT_FALLBACK;
674
- }
391
+ return TERMINAL_FONT_FALLBACK;
392
+ }
393
+ //#endregion
394
+ //#region src/client/api.ts
395
+ /**
396
+ * Browser-side API client for the /api/dsh-ssh route family. The only data
397
+ * access path the panel components use — plain fetch/WebSocket, same origin.
398
+ */
399
+ /** Error carrying the route's JSON error message. */
400
+ var SshApiError = class extends Error {
401
+ constructor(message) {
402
+ super(message);
403
+ this.name = "SshApiError";
404
+ }
405
+ };
406
+ /** Extract the `.error` message from a JSON error body; undefined when absent. */
407
+ function extractError(text) {
408
+ if (text === "") return void 0;
409
+ try {
410
+ const parsed = JSON.parse(text);
411
+ return typeof parsed?.error === "string" ? parsed.error : void 0;
412
+ } catch {
413
+ return;
414
+ }
415
+ }
416
+ /** Parse a JSON response or throw an SshApiError. */
417
+ async function readJson(response) {
418
+ let body;
419
+ try {
420
+ body = await response.json();
421
+ } catch {
422
+ if (response.status === 404) throw new SshApiError(tt("error.disabled"));
423
+ throw new SshApiError(`HTTP ${response.status}: invalid JSON response`);
424
+ }
425
+ if (!response.ok) throw new SshApiError(typeof body === "object" && body !== null && typeof body.error === "string" ? body.error : response.status === 404 ? tt("error.disabled") : `HTTP ${response.status}`);
426
+ return body;
427
+ }
428
+ /** Query-string helper. */
429
+ function query(params) {
430
+ const search = new URLSearchParams();
431
+ for (const [key, value] of Object.entries(params)) if (value !== void 0 && value !== "") search.set(key, String(value));
432
+ const text = search.toString();
433
+ return text === "" ? "" : "?" + text;
434
+ }
435
+ /** The browser half's only data entry point. */
436
+ var SshApi = class {
437
+ async listHosts(queryText) {
438
+ return (await readJson(await fetch(SSH_API.hosts + query({ query: queryText })))).hosts;
439
+ }
440
+ async createHost(payload) {
441
+ return (await readJson(await fetch(SSH_API.hosts, {
442
+ method: "POST",
443
+ headers: { "content-type": "application/json" },
444
+ body: JSON.stringify(payload)
445
+ }))).host;
446
+ }
447
+ async updateHost(alias, patch) {
448
+ return (await readJson(await fetch(SSH_API.hosts + query({ alias }), {
449
+ method: "PATCH",
450
+ headers: { "content-type": "application/json" },
451
+ body: JSON.stringify(patch)
452
+ }))).host;
453
+ }
454
+ async deleteHost(alias) {
455
+ await readJson(await fetch(SSH_API.hosts + query({ alias }), { method: "DELETE" }));
456
+ }
457
+ async importSshConfig() {
458
+ return (await readJson(await fetch(SSH_API.importSshConfig, { method: "POST" }))).result;
459
+ }
460
+ async testHost(alias) {
461
+ return (await readJson(await fetch(SSH_API.test, {
462
+ method: "POST",
463
+ headers: { "content-type": "application/json" },
464
+ body: JSON.stringify({ alias })
465
+ }))).result;
466
+ }
467
+ async exec(alias, command, timeoutMs) {
468
+ return (await readJson(await fetch(SSH_API.exec, {
469
+ method: "POST",
470
+ headers: { "content-type": "application/json" },
471
+ body: JSON.stringify({
472
+ alias,
473
+ command,
474
+ timeoutMs
475
+ })
476
+ }))).result;
477
+ }
478
+ async cluster(options) {
479
+ return (await readJson(await fetch(SSH_API.cluster, {
480
+ method: "POST",
481
+ headers: { "content-type": "application/json" },
482
+ body: JSON.stringify(options)
483
+ }))).results;
484
+ }
485
+ async ls(alias, path) {
486
+ return (await readJson(await fetch(SSH_API.ls + query({
487
+ alias,
488
+ path
489
+ })))).entries;
490
+ }
491
+ async listTunnels() {
492
+ return (await readJson(await fetch(SSH_API.tunnel, {
493
+ method: "POST",
494
+ headers: { "content-type": "application/json" },
495
+ body: JSON.stringify({ action: "list" })
496
+ }))).tunnels;
497
+ }
498
+ async startTunnel(options) {
499
+ return (await readJson(await fetch(SSH_API.tunnel, {
500
+ method: "POST",
501
+ headers: { "content-type": "application/json" },
502
+ body: JSON.stringify({
503
+ action: "start",
504
+ ...options
505
+ })
506
+ }))).tunnel;
507
+ }
508
+ async stopTunnel(tunnelId) {
509
+ return (await readJson(await fetch(SSH_API.tunnel, {
510
+ method: "POST",
511
+ headers: { "content-type": "application/json" },
512
+ body: JSON.stringify({
513
+ action: "stop",
514
+ tunnelId
515
+ })
516
+ }))).ok;
517
+ }
518
+ async stopAllTunnels(alias) {
519
+ return (await readJson(await fetch(SSH_API.tunnel, {
520
+ method: "POST",
521
+ headers: { "content-type": "application/json" },
522
+ body: JSON.stringify({
523
+ action: "stop-all",
524
+ alias
525
+ })
526
+ }))).stopped;
527
+ }
528
+ /**
529
+ * Upload one file (raw bytes) to a remote path. Progress arrives through
530
+ * the NDJSON response stream; resolves when the result frame lands.
531
+ */
532
+ async uploadFile(file, alias, remotePath, onProgress) {
533
+ const response = await fetch(SSH_API.upload + query({
534
+ alias,
535
+ remotePath
536
+ }), {
537
+ method: "POST",
538
+ body: file
539
+ });
540
+ if (!response.ok || response.body === null) throw new SshApiError(extractError(await response.text().catch(() => "")) ?? `upload failed: HTTP ${response.status}`);
541
+ const reader = response.body.getReader();
542
+ const decoder = new TextDecoder();
543
+ let buffer = "";
544
+ let finalError;
545
+ let sawResult = false;
546
+ let transferredBytes = 0;
547
+ for (;;) {
548
+ const { done, value } = await reader.read();
549
+ if (done) break;
550
+ buffer += decoder.decode(value, { stream: true });
551
+ const lines = buffer.split("\n");
552
+ buffer = lines.pop() ?? "";
553
+ for (const line of lines) {
554
+ if (line.trim() === "") continue;
555
+ let parsed;
556
+ try {
557
+ parsed = JSON.parse(line);
558
+ } catch {
559
+ continue;
560
+ }
561
+ if (parsed.type === "progress") onProgress?.(parsed.progress);
562
+ else if (parsed.type === "result") {
563
+ sawResult = true;
564
+ if (parsed.ok) transferredBytes = parsed.transferredBytes ?? 0;
565
+ finalError = parsed.ok ? void 0 : parsed.error ?? "upload failed";
566
+ }
567
+ }
568
+ }
569
+ if (finalError !== void 0) throw new SshApiError(finalError);
570
+ if (!sawResult) throw new SshApiError("upload ended without a result frame — the transfer did not complete");
571
+ return { transferredBytes };
572
+ }
573
+ /**
574
+ * Download a remote file with client-side progress. Streams straight to
575
+ * disk when the File System Access API is available (no full-file RAM
576
+ * copy); otherwise falls back to an in-memory Blob.
577
+ */
578
+ async downloadFile(alias, remotePath, onProgress) {
579
+ const response = await fetch(SSH_API.download + query({
580
+ alias,
581
+ remotePath
582
+ }));
583
+ if (!response.ok || response.body === null) throw new SshApiError(extractError(await response.text().catch(() => "")) ?? `download failed: HTTP ${response.status}`);
584
+ const total = Number(response.headers.get("content-length") ?? "0");
585
+ const disposition = response.headers.get("content-disposition") ?? "";
586
+ const filename = /filename="([^"]+)"/.exec(disposition)?.[1] ?? remotePath.split("/").pop() ?? "download";
587
+ const reader = response.body.getReader();
588
+ const picker = typeof window !== "undefined" ? window.showSaveFilePicker : void 0;
589
+ let streamed = false;
590
+ let writable;
591
+ const chunks = [];
592
+ let received = 0;
593
+ const progress = () => {
594
+ onProgress?.({
595
+ phase: "transferring",
596
+ file: remotePath,
597
+ transferred: received,
598
+ total,
599
+ percent: total > 0 ? Math.round(received / total * 1e3) / 10 : 0
600
+ });
601
+ };
602
+ try {
603
+ if (picker !== void 0) {
604
+ writable = await (await picker.call(window, { suggestedName: filename })).createWritable();
605
+ streamed = true;
606
+ }
607
+ } catch {}
608
+ for (;;) {
609
+ const { done, value } = await reader.read();
610
+ if (done) break;
611
+ if (writable !== void 0) await writable.write(value);
612
+ else chunks.push(value);
613
+ received += value.length;
614
+ progress();
615
+ }
616
+ if (writable !== void 0) await writable.close();
617
+ onProgress?.({
618
+ phase: "done",
619
+ file: remotePath,
620
+ transferred: received,
621
+ total: received > 0 ? received : total,
622
+ percent: 100
623
+ });
624
+ return {
625
+ blob: streamed ? void 0 : new Blob(chunks),
626
+ filename,
627
+ streamed,
628
+ bytes: received
629
+ };
630
+ }
631
+ /** Open a WebSocket terminal session. */
632
+ openTerminal(alias, cols, rows) {
633
+ const url = (window.location.protocol === "https:" ? "wss" : "ws") + "://" + window.location.host + SSH_API.terminal + query({
634
+ alias,
635
+ cols,
636
+ rows
637
+ });
638
+ const socket = new WebSocket(url);
639
+ const connection = {
640
+ onReady: void 0,
641
+ onOutput: void 0,
642
+ onExit: void 0,
643
+ onAuthPrompt: void 0,
644
+ send: (data) => {
645
+ if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify({
646
+ type: "input",
647
+ data
648
+ }));
649
+ },
650
+ resize: (cols, rows) => {
651
+ if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify({
652
+ type: "resize",
653
+ cols,
654
+ rows
655
+ }));
656
+ },
657
+ sendAuthResponse: (responses) => {
658
+ if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify({
659
+ type: "auth_response",
660
+ responses
661
+ }));
662
+ },
663
+ close: () => {
664
+ try {
665
+ socket.close();
666
+ } catch {}
667
+ }
668
+ };
669
+ socket.onmessage = (event) => {
670
+ let frame;
671
+ try {
672
+ frame = JSON.parse(event.data);
673
+ } catch {
674
+ return;
675
+ }
676
+ if (frame.type === "ready") connection.onReady?.();
677
+ else if (frame.type === "output") connection.onOutput?.(frame.data);
678
+ else if (frame.type === "exit") connection.onExit?.(frame.code, frame.error);
679
+ else if (frame.type === "auth_prompt") connection.onAuthPrompt?.(frame.name, frame.instructions, frame.prompts);
680
+ };
681
+ socket.onclose = () => {
682
+ connection.onExit?.(null, "connection closed");
683
+ };
684
+ socket.onerror = () => {
685
+ connection.onExit?.(null, "connection error");
686
+ };
687
+ return connection;
688
+ }
689
+ };
675
690
  //#endregion
676
691
  //#region \0dsh-css:packages/dsh-ssh/src/client/panel/panel.module.css.mjs
677
692
  const css = "[data-pane=conversation],[class*=centerCol]{position:relative}[data-dsh-ssh-view]{z-index:60;background:var(--dsw-alias-bg-base);display:none;position:absolute;inset:0}html[data-dsh-ssh-active]:not([data-dsh-taskboard-active]) [data-dsh-ssh-view]{display:block}html[data-dsh-ssh-active]:not([data-dsh-taskboard-active]) [data-pane=conversation]>:not([data-dsh-ssh-view]),html[data-dsh-ssh-active]:not([data-dsh-taskboard-active]) [class*=centerCol]>:not([data-dsh-ssh-view]){display:none!important}.mL8Uca_entry{box-sizing:border-box;width:100%;min-height:36px;color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-radius:8px;align-items:center;gap:10px;padding:0 10px;font-size:13px;display:flex}.mL8Uca_entry:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.mL8Uca_entry[data-active]{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary);font-weight:600}.mL8Uca_entryIcon{flex:none;justify-content:center;align-items:center;width:24px;height:24px;display:inline-flex}.mL8Uca_entryIcon svg{width:18px;height:18px;display:block}.mL8Uca_entryLabel{text-overflow:ellipsis;overflow:hidden}[data-dsh-frame][data-sidebar-collapsed] .mL8Uca_entry{border-radius:50%;justify-content:center;width:36px;min-height:36px;margin:0 auto 12px;padding:0}[data-dsh-frame][data-sidebar-collapsed] .mL8Uca_entryLabel{display:none}.mL8Uca_view{overflow:hidden}.mL8Uca_panel{background:var(--dsw-alias-bg-base);min-width:0;height:100%;min-height:0;color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family);flex-direction:column;gap:10px;padding:14px 16px 16px;display:flex}.mL8Uca_panelHeader{flex:none;align-items:center;gap:10px;display:flex}.mL8Uca_panelTitle{color:var(--dsw-alias-label-primary);white-space:nowrap;flex:1;margin:0;font-size:16px;font-weight:700}.mL8Uca_backButton{align-items:center;gap:4px;display:inline-flex}.mL8Uca_tabBar{border-bottom:1px solid var(--dsw-alias-border-l1);flex:none;gap:2px;display:flex}.mL8Uca_tab{color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-bottom:2px solid #0000;border-radius:6px 6px 0 0;padding:7px 14px;font-size:13px}.mL8Uca_tab:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.mL8Uca_tab[data-active]{color:var(--dsw-alias-label-primary);border-bottom-color:var(--dsw-alias-state-business-primary);font-weight:600}.mL8Uca_panelContent{flex-direction:column;flex:1;min-height:0;display:flex;overflow:hidden}.mL8Uca_tabBody{flex-direction:column;flex:1;gap:10px;min-height:0;display:flex;overflow-y:auto}.mL8Uca_fillBody{flex-direction:column;flex:1;gap:10px;min-height:0;display:flex;overflow:hidden}.mL8Uca_toolbar,.mL8Uca_controls{flex-wrap:wrap;flex:none;align-items:center;gap:8px;display:flex}.mL8Uca_controls .mL8Uca_input{flex:0 260px;min-width:150px}.mL8Uca_toolbarSpacer{flex:1}.mL8Uca_search{min-width:120px;color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;outline:none;flex:0 260px;padding:6px 10px;font-size:13px}.mL8Uca_search::placeholder{color:var(--dsw-alias-label-tertiary)}.mL8Uca_groupBySelect{color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;outline:none;padding:6px 8px;font-size:13px}.mL8Uca_groupSection+.mL8Uca_groupSection{border-top:1px solid var(--dsw-alias-border-l1)}.mL8Uca_groupHeader{background:var(--dsw-alias-bg-base,var(--dsw-specific-input-major));align-items:center;gap:8px;padding:4px 8px;display:flex;position:sticky;top:0}.mL8Uca_groupToggle{min-width:0;color:var(--dsw-alias-label-primary);cursor:pointer;text-align:left;background:0 0;border:none;border-radius:6px;flex:1;align-items:center;gap:6px;padding:4px 6px;font-size:12.5px;font-weight:500;display:flex}.mL8Uca_groupToggle:hover{background:var(--dsw-alias-interactive-bg-hover)}.mL8Uca_groupChevron{border-right:1.5px solid var(--dsw-alias-label-tertiary);border-bottom:1.5px solid var(--dsw-alias-label-tertiary);flex:none;width:7px;height:7px;margin-bottom:2px;transition:transform .12s;transform:rotate(45deg)}.mL8Uca_groupChevron[data-collapsed]{margin-bottom:0;transform:rotate(-45deg)}.mL8Uca_groupName{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.mL8Uca_groupCount{color:var(--dsw-alias-label-tertiary);flex:none;font-weight:400}.mL8Uca_tableWrap{border:1px solid var(--dsw-alias-border-l1);border-radius:10px;flex:1;min-height:0;overflow:auto}.mL8Uca_table{border-collapse:collapse;width:100%;font-size:12.5px}.mL8Uca_table th{z-index:1;text-align:left;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);border-bottom:1px solid var(--dsw-alias-border-l1);white-space:nowrap;padding:8px 10px;font-weight:600;position:sticky;top:0}.mL8Uca_table td{border-bottom:1px solid var(--dsw-alias-separator-primary);vertical-align:top;padding:7px 10px}.mL8Uca_table tbody tr:last-child td{border-bottom:none}.mL8Uca_table tbody tr:hover td{background:var(--dsw-alias-interactive-bg-hover)}.mL8Uca_mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}.mL8Uca_cellMuted{color:var(--dsw-alias-label-tertiary)}.mL8Uca_actions{white-space:nowrap;align-items:center;gap:8px;display:flex}.mL8Uca_inlineTest{color:var(--dsw-alias-label-secondary);align-items:center;gap:6px;font-size:11.5px;display:inline-flex}.mL8Uca_inlineTest[data-status=ok]{color:var(--dsw-alias-state-success-primary)}.mL8Uca_inlineTest[data-status=fail]{color:var(--dsw-alias-state-error-primary)}.mL8Uca_badge{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);white-space:nowrap;border-radius:999px;padding:1px 8px;font-size:11px;line-height:1.6;display:inline-block}.mL8Uca_badge[data-kind=key]{color:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary)}.mL8Uca_badge[data-kind=password]{color:var(--dsw-alias-state-warn-primary);border-color:var(--dsw-alias-state-warn-primary)}.mL8Uca_badge[data-kind=agent],.mL8Uca_badge[data-status=ok]{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}.mL8Uca_badge[data-status=fail]{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}.mL8Uca_badge[data-status=timeout]{color:var(--dsw-alias-state-warn-primary);border-color:var(--dsw-alias-state-warn-primary)}.mL8Uca_primaryButton{color:var(--dsw-alias-label-primary-foreground);background:var(--dsw-alias-button-info-fill);cursor:pointer;white-space:nowrap;border:none;border-radius:8px;padding:6px 14px;font-size:13px;font-weight:600}.mL8Uca_primaryButton:hover:not(:disabled){background:var(--dsw-alias-button-info-hover)}.mL8Uca_primaryButton:disabled{opacity:.5;cursor:default}.mL8Uca_ghostButton{color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;white-space:nowrap;background:0 0;border-radius:8px;padding:5px 12px;font-size:12px}.mL8Uca_ghostButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.mL8Uca_ghostButton:disabled{opacity:.45;cursor:default}.mL8Uca_iconButton{width:26px;height:26px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:6px;justify-content:center;align-items:center;padding:0;font-size:13px;display:inline-flex}.mL8Uca_iconButton:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.mL8Uca_linkButton{color:var(--dsw-alias-state-business-primary);cursor:pointer;white-space:nowrap;background:0 0;border:none;padding:0;font-size:12px}.mL8Uca_linkButton:hover:not(:disabled){text-decoration:underline}.mL8Uca_linkButton:disabled{opacity:.45;cursor:default}.mL8Uca_linkButton[data-danger]{color:var(--dsw-alias-state-error-primary)}.mL8Uca_spinner{border:2px solid var(--dsw-alias-state-business-primary);vertical-align:-1px;border-top-color:#0000;border-radius:50%;flex:none;width:11px;height:11px;animation:.8s linear infinite mL8Uca_dshSshSpin;display:inline-block}@keyframes mL8Uca_dshSshSpin{to{transform:rotate(360deg)}}.mL8Uca_banner{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere;border-radius:8px;padding:8px 12px;font-size:12.5px;line-height:1.5}.mL8Uca_banner[data-kind=ok]{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}.mL8Uca_banner[data-kind=error]{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}.mL8Uca_banner[data-kind=info]{color:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary)}.mL8Uca_empty,.mL8Uca_loading{text-align:center;color:var(--dsw-alias-label-tertiary);padding:28px 12px;font-size:12.5px}.mL8Uca_modalBackdrop{z-index:40;background:var(--dsw-alias-bg-mask-1);justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.mL8Uca_modal{background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);width:min(560px,100vw - 48px);max-height:calc(100vh - 96px);box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-primary);border-radius:14px;flex-direction:column;gap:12px;padding:18px;display:flex;overflow-y:auto}.mL8Uca_modalTitle{margin:0;font-size:15px;font-weight:700}.mL8Uca_modalFooter{justify-content:flex-end;gap:10px;margin-top:4px;display:flex}.mL8Uca_formError{color:var(--dsw-alias-state-error-primary);margin:0;font-size:12px}.mL8Uca_hint{color:var(--dsw-alias-label-tertiary);font-size:11.5px}.mL8Uca_field{flex-direction:column;gap:5px;display:flex}.mL8Uca_fieldLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600}.mL8Uca_input{color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);resize:vertical;border-radius:8px;outline:none;padding:7px 10px;font-family:inherit;font-size:13px}.mL8Uca_input:focus{border-color:var(--dsw-alias-state-business-primary)}.mL8Uca_input::placeholder{color:var(--dsw-alias-label-tertiary)}.mL8Uca_input:disabled{opacity:.55}.mL8Uca_formRow{grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px 12px;display:grid}.mL8Uca_radioRow{align-items:center;gap:16px;display:flex}.mL8Uca_radioLabel{color:var(--dsw-alias-label-primary);cursor:pointer;align-items:center;gap:6px;font-size:13px;display:inline-flex}.mL8Uca_termBody{flex-direction:column;flex:1;gap:8px;min-height:0;display:flex;overflow:hidden}.mL8Uca_termWrap{border:1px solid var(--dsw-alias-border-l1);background:#0b0e14;border-radius:10px;flex:1;min-height:0;position:relative;overflow:hidden}.mL8Uca_termContainer{position:absolute;inset:8px 10px}.mL8Uca_termPlaceholder{z-index:2;text-align:center;color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-base);justify-content:center;align-items:center;padding:0 24px;font-size:12.5px;display:flex;position:absolute;inset:0}.mL8Uca_hiddenFile{display:none}.mL8Uca_browsePanel{border:1px solid var(--dsw-alias-border-l1);border-radius:10px;flex-direction:column;flex:none;display:flex;overflow:hidden}.mL8Uca_browseHeader{background:var(--dsw-alias-bg-layer-2);border-bottom:1px solid var(--dsw-alias-border-l1);align-items:center;gap:8px;padding:8px 10px;display:flex}.mL8Uca_browsePath{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-secondary);flex:1;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;overflow:hidden}.mL8Uca_browseList{max-height:240px;overflow-y:auto}.mL8Uca_dirRow{text-align:left;width:100%;font:inherit;color:var(--dsw-alias-label-primary);border:none;border-bottom:1px solid var(--dsw-alias-separator-primary);cursor:pointer;background:0 0;grid-template-columns:minmax(0,1fr) auto auto;gap:12px;padding:5px 10px;font-size:12.5px;display:grid}.mL8Uca_dirRow:hover{background:var(--dsw-alias-interactive-bg-hover)}.mL8Uca_dirRow[data-type=dir] .mL8Uca_dirName{color:var(--dsw-alias-state-business-primary)}.mL8Uca_dirRow[data-up]{color:var(--dsw-alias-label-secondary)}.mL8Uca_dirName{text-overflow:ellipsis;white-space:nowrap;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;overflow:hidden}.mL8Uca_dirType{color:var(--dsw-alias-label-tertiary);white-space:nowrap}.mL8Uca_dirSize{color:var(--dsw-alias-label-tertiary);white-space:nowrap;text-align:right}.mL8Uca_transferBlock{flex-direction:column;flex:none;gap:6px;display:flex}.mL8Uca_progressMeta{color:var(--dsw-alias-label-secondary);flex-wrap:wrap;gap:12px;font-size:12px;display:flex}.mL8Uca_progressTrack{background:var(--dsw-alias-interactive-bg-hover);border-radius:999px;height:8px;overflow:hidden}.mL8Uca_progressBar{background:var(--dsw-alias-state-business-primary);border-radius:999px;height:100%;transition:width .12s linear}.mL8Uca_tunnelList{flex-direction:column;flex:none;gap:8px;display:flex}.mL8Uca_tunnelRow{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;align-items:center;gap:10px;padding:8px 12px;display:flex}.mL8Uca_tunnelRow[data-state=forwarding]{border-color:var(--dsw-alias-state-success-primary)}.mL8Uca_tunnelRow[data-state=connecting]{border-color:var(--dsw-alias-state-warn-primary)}.mL8Uca_tunnelRow[data-state=failed]{border-color:var(--dsw-alias-state-error-primary)}.mL8Uca_tunnelLabel{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12.5px;overflow:hidden}.mL8Uca_formCard{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;flex-direction:column;flex:none;gap:10px;padding:12px;display:flex}.mL8Uca_clusterForm{flex-direction:column;flex:none;gap:8px;display:flex}.mL8Uca_clusterFilters{grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:10px;display:grid}.mL8Uca_commandInput{min-height:96px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}.mL8Uca_cellDetails summary{cursor:pointer;color:var(--dsw-alias-label-secondary);font-size:12px}.mL8Uca_cellPre{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);white-space:pre-wrap;word-break:break-all;border-radius:6px;max-height:180px;margin:4px 0 0;padding:6px 8px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px;overflow:auto}";
@@ -15835,11 +15850,23 @@ window.__ModuleLoader__.load({
15835
15850
  * @param controller - the panel controller driving the view.
15836
15851
  * @param api - the SSH API client the tabs operate through.
15837
15852
  * @param terminalFont - live terminal-font setting source (issue #577).
15853
+ * @param locale - locale-change source; when given, re-renders an open panel
15854
+ * on a Language switch.
15838
15855
  * @returns disposer unmounting the tree and restoring the column.
15839
15856
  */
15840
- function mountPanel(controller, api, terminalFont) {
15857
+ function mountPanel(controller, api, terminalFont, locale) {
15841
15858
  let root;
15842
15859
  let container;
15860
+ let unsubscribeLocale;
15861
+ try {
15862
+ unsubscribeLocale = locale?.subscribe(() => {
15863
+ if (root !== void 0) root.render(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SshPanel, {
15864
+ controller,
15865
+ api,
15866
+ terminalFont
15867
+ }));
15868
+ });
15869
+ } catch {}
15843
15870
  const ensure = () => {
15844
15871
  if (container !== void 0) {
15845
15872
  if (container.isConnected) return;
@@ -15896,6 +15923,7 @@ window.__ModuleLoader__.load({
15896
15923
  document.removeEventListener(ACTIVATE_EVENT, onOtherActivate);
15897
15924
  waitObserver.disconnect();
15898
15925
  unsubscribe();
15926
+ unsubscribeLocale?.();
15899
15927
  document.documentElement.removeAttribute(ACTIVE_ATTR);
15900
15928
  root?.unmount();
15901
15929
  root = void 0;
@@ -15960,11 +15988,23 @@ window.__ModuleLoader__.load({
15960
15988
  entry.setAttribute("data-dsh-part", "sidebar-entry");
15961
15989
  }
15962
15990
  entry.className = options.css["entry"] ?? "";
15963
- entry.setAttribute("aria-label", options.label());
15964
- if (options.tooltip !== void 0) entry.setAttribute("title", options.tooltip());
15965
- entry.innerHTML = "<span class=\"" + (options.css["entryIcon"] ?? "") + "\">" + options.icon + "</span><span class=\"" + (options.css["entryLabel"] ?? "") + "\">" + options.label() + "</span>";
15991
+ const labelSpan = document.createElement("span");
15992
+ labelSpan.className = options.css["entryLabel"] ?? "";
15993
+ const iconSpan = document.createElement("span");
15994
+ iconSpan.className = options.css["entryIcon"] ?? "";
15995
+ iconSpan.innerHTML = options.icon;
15996
+ entry.append(iconSpan, labelSpan);
15997
+ const applyLabel = () => {
15998
+ entry.setAttribute("aria-label", options.label());
15999
+ if (options.tooltip !== void 0) entry.setAttribute("title", options.tooltip());
16000
+ labelSpan.textContent = options.label();
16001
+ };
16002
+ applyLabel();
15966
16003
  entry.addEventListener("click", options.onToggle);
15967
- return entry;
16004
+ return {
16005
+ entry,
16006
+ applyLabel
16007
+ };
15968
16008
  }
15969
16009
  /** Re-insert the entry after the New Session row (before the browser region). */
15970
16010
  function placeEntry(root, entry, options) {
@@ -15987,9 +16027,13 @@ window.__ModuleLoader__.load({
15987
16027
  */
15988
16028
  function mountSidebarEntry$1(options) {
15989
16029
  if (typeof document !== "undefined" && document.querySelector(options.rowSelector) !== null) return () => {};
15990
- const entry = createEntry(options);
16030
+ const { entry, applyLabel } = createEntry(options);
15991
16031
  let root;
15992
16032
  let placed = false;
16033
+ let unsubscribeRefresh;
16034
+ if (options.refresh !== void 0) try {
16035
+ unsubscribeRefresh = options.refresh.subscribe(applyLabel);
16036
+ } catch {}
15993
16037
  const tryPlace = () => {
15994
16038
  if (root !== void 0 && !root.isConnected) {
15995
16039
  rootObserver.disconnect();
@@ -16038,6 +16082,7 @@ window.__ModuleLoader__.load({
16038
16082
  return () => {
16039
16083
  waitObserver.disconnect();
16040
16084
  rootObserver.disconnect();
16085
+ unsubscribeRefresh?.();
16041
16086
  unsubscribeActive?.();
16042
16087
  entry.remove();
16043
16088
  };
@@ -16052,9 +16097,11 @@ window.__ModuleLoader__.load({
16052
16097
  * Mount the sidebar entry, waiting for the shell to render and self-healing
16053
16098
  * on later React re-renders.
16054
16099
  * @param controller - the panel controller the entry toggles.
16100
+ * @param locale - locale-change source; when given, re-applies the label on
16101
+ * a Language switch (the plain-DOM row otherwise keeps the mount-time copy).
16055
16102
  * @returns disposer removing the entry and its observers.
16056
16103
  */
16057
- function mountSidebarEntry(controller) {
16104
+ function mountSidebarEntry(controller, locale) {
16058
16105
  return mountSidebarEntry$1({
16059
16106
  rowAttribute: "data-dsh-ssh-entry",
16060
16107
  rowSelector: ENTRY_SELECTOR,
@@ -16063,6 +16110,7 @@ window.__ModuleLoader__.load({
16063
16110
  css: panel_module_css_default,
16064
16111
  label: () => tt("entry.label"),
16065
16112
  tooltip: () => tt("entry.tooltip"),
16113
+ refresh: locale === void 0 ? void 0 : { subscribe: (listener) => locale.subscribe(listener) },
16066
16114
  onToggle: () => {
16067
16115
  controller.toggle();
16068
16116
  },
@@ -16082,7 +16130,7 @@ window.__ModuleLoader__.load({
16082
16130
  /** The building package's version, when the bundle carries it. */
16083
16131
  function bakedVersion() {
16084
16132
  try {
16085
- return "0.3.6";
16133
+ return "0.3.10";
16086
16134
  } catch {
16087
16135
  return;
16088
16136
  }
@@ -16172,6 +16220,9 @@ window.__ModuleLoader__.load({
16172
16220
  return () => {};
16173
16221
  }
16174
16222
  }, "dsh-ssh: dictionaries");
16223
+ try {
16224
+ setRuntimeTranslate(ctx.locale.bind(NS));
16225
+ } catch {}
16175
16226
  const controller = new PanelController();
16176
16227
  const api = new SshApi();
16177
16228
  const scope = (ctx.get("webUiSettings") ?? ctx.settingsScope).bind({ namespace: SETTINGS_NS });
@@ -16184,8 +16235,8 @@ window.__ModuleLoader__.load({
16184
16235
  };
16185
16236
  const disposers = [];
16186
16237
  try {
16187
- disposers.push(mountSidebarEntry(controller));
16188
- disposers.push(mountPanel(controller, api, terminalFont));
16238
+ disposers.push(mountSidebarEntry(controller, ctx.locale));
16239
+ disposers.push(mountPanel(controller, api, terminalFont, ctx.locale));
16189
16240
  } catch (error) {
16190
16241
  console.warn("[dsh-ssh] mount failed:", error);
16191
16242
  }