@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 +361 -310
- package/lib/client.js.map +1 -1
- package/lib/index.js +19 -14
- package/lib/types/client/index.d.ts +2 -1
- package/lib/types/client/locales.d.ts +1 -0
- package/lib/types/client/mount.d.ts +4 -1
- package/lib/types/client/panel/helpers.d.ts +2 -0
- package/lib/types/client/sidebar-entry-core.d.ts +10 -0
- package/lib/types/client/sidebar-entry.d.ts +7 -1
- package/lib/types/index.d.ts +2 -1
- package/package.json +17 -17
- package/src/client/api.ts +5 -1
- package/src/client/index.ts +12 -3
- package/src/client/locales.ts +2 -0
- package/src/client/mount.tsx +11 -1
- package/src/client/panel/helpers.ts +14 -0
- package/src/client/sidebar-entry-core.ts +33 -7
- package/src/client/sidebar-entry.ts +7 -1
- package/src/dsh-home.ts +1 -1
- package/src/index.ts +27 -14
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
|
-
|
|
15964
|
-
|
|
15965
|
-
|
|
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
|
|
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.
|
|
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
|
}
|