@linxin666/dsh-ssh 0.3.5 → 0.3.9
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 +404 -301
- package/lib/client.js.map +1 -1
- package/lib/index.js +77 -24
- package/lib/types/client/api.d.ts +7 -0
- package/lib/types/client/index.d.ts +2 -1
- package/lib/types/client/locales.d.ts +6 -0
- package/lib/types/engine/connection-pool.d.ts +7 -2
- package/lib/types/engine/pty.d.ts +2 -2
- package/lib/types/engine.d.ts +3 -3
- package/lib/types/index.d.ts +2 -1
- package/lib/types/protocol.d.ts +11 -0
- package/package.json +17 -17
- package/src/client/api.ts +16 -1
- package/src/client/index.ts +4 -1
- package/src/client/locales.ts +12 -0
- package/src/client/panel/TerminalTab.tsx +61 -1
- package/src/dsh-home.ts +1 -1
- package/src/engine/connection-pool.ts +30 -4
- package/src/engine/pty.ts +8 -3
- package/src/engine.ts +8 -3
- package/src/index.ts +27 -14
- package/src/protocol.ts +7 -0
- package/src/routes.ts +33 -2
package/lib/client.js
CHANGED
|
@@ -24,294 +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
|
-
send: (data) => {
|
|
276
|
-
if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify({
|
|
277
|
-
type: "input",
|
|
278
|
-
data
|
|
279
|
-
}));
|
|
280
|
-
},
|
|
281
|
-
resize: (cols, rows) => {
|
|
282
|
-
if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify({
|
|
283
|
-
type: "resize",
|
|
284
|
-
cols,
|
|
285
|
-
rows
|
|
286
|
-
}));
|
|
287
|
-
},
|
|
288
|
-
close: () => {
|
|
289
|
-
try {
|
|
290
|
-
socket.close();
|
|
291
|
-
} catch {}
|
|
292
|
-
}
|
|
293
|
-
};
|
|
294
|
-
socket.onmessage = (event) => {
|
|
295
|
-
let frame;
|
|
296
|
-
try {
|
|
297
|
-
frame = JSON.parse(event.data);
|
|
298
|
-
} catch {
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
if (frame.type === "ready") connection.onReady?.();
|
|
302
|
-
else if (frame.type === "output") connection.onOutput?.(frame.data);
|
|
303
|
-
else if (frame.type === "exit") connection.onExit?.(frame.code, frame.error);
|
|
304
|
-
};
|
|
305
|
-
socket.onclose = () => {
|
|
306
|
-
connection.onExit?.(null, "connection closed");
|
|
307
|
-
};
|
|
308
|
-
socket.onerror = () => {
|
|
309
|
-
connection.onExit?.(null, "connection error");
|
|
310
|
-
};
|
|
311
|
-
return connection;
|
|
312
|
-
}
|
|
313
|
-
};
|
|
314
|
-
//#endregion
|
|
315
27
|
//#region src/client/locales.ts
|
|
316
28
|
/**
|
|
317
29
|
* dsh-ssh surface copy: zh is the key source, en mirrors every key.
|
|
@@ -396,6 +108,11 @@ window.__ModuleLoader__.load({
|
|
|
396
108
|
"terminal.ready": "终端已连接({alias})",
|
|
397
109
|
"terminal.exited": "终端已退出({alias})",
|
|
398
110
|
"terminal.error": "终端错误:{error}",
|
|
111
|
+
"terminal.auth.title": "二次身份验证 (2FA / 交互式认证)",
|
|
112
|
+
"terminal.auth.submit": "提交验证码",
|
|
113
|
+
"terminal.auth.cancel": "取消",
|
|
114
|
+
"terminal.auth.placeholder": "请输入验证码或口令",
|
|
115
|
+
"terminal.auth.hint": "服务器请求进行交互式二次验证(输入仅本次生效,不落盘):",
|
|
399
116
|
"transfer.selectHost": "选择主机",
|
|
400
117
|
"transfer.remotePath": "远程路径",
|
|
401
118
|
"transfer.remotePathHint": "如 /tmp/ 或 /home/user/app.tar.gz",
|
|
@@ -455,7 +172,8 @@ window.__ModuleLoader__.load({
|
|
|
455
172
|
"common.loading": "加载中…",
|
|
456
173
|
"common.error": "出错:{error}",
|
|
457
174
|
"common.refresh": "刷新",
|
|
458
|
-
"common.confirm": "确定"
|
|
175
|
+
"common.confirm": "确定",
|
|
176
|
+
"error.disabled": "SSH 插件在宿主端未启用。请前往「设置 → Web 插件 → SSH」打开 enabled 开关。"
|
|
459
177
|
};
|
|
460
178
|
const en = {
|
|
461
179
|
"entry.label": "SSH",
|
|
@@ -537,6 +255,11 @@ window.__ModuleLoader__.load({
|
|
|
537
255
|
"terminal.ready": "Terminal connected ({alias})",
|
|
538
256
|
"terminal.exited": "Terminal exited ({alias})",
|
|
539
257
|
"terminal.error": "Terminal error: {error}",
|
|
258
|
+
"terminal.auth.title": "Two-Factor Authentication (2FA)",
|
|
259
|
+
"terminal.auth.submit": "Submit Code",
|
|
260
|
+
"terminal.auth.cancel": "Cancel",
|
|
261
|
+
"terminal.auth.placeholder": "Enter code or passphrase",
|
|
262
|
+
"terminal.auth.hint": "The server requested interactive 2FA (session only, never saved):",
|
|
540
263
|
"transfer.selectHost": "Select host",
|
|
541
264
|
"transfer.remotePath": "Remote path",
|
|
542
265
|
"transfer.remotePathHint": "e.g. /tmp/ or /home/user/app.tar.gz",
|
|
@@ -596,7 +319,8 @@ window.__ModuleLoader__.load({
|
|
|
596
319
|
"common.loading": "Loading…",
|
|
597
320
|
"common.error": "Error: {error}",
|
|
598
321
|
"common.refresh": "Refresh",
|
|
599
|
-
"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."
|
|
600
324
|
};
|
|
601
325
|
/** Tiny interpolation: {name} -> value. */
|
|
602
326
|
function t(dictionary, key, values) {
|
|
@@ -652,8 +376,305 @@ window.__ModuleLoader__.load({
|
|
|
652
376
|
if (value !== "") return value;
|
|
653
377
|
}
|
|
654
378
|
}
|
|
655
|
-
return TERMINAL_FONT_FALLBACK;
|
|
656
|
-
}
|
|
379
|
+
return TERMINAL_FONT_FALLBACK;
|
|
380
|
+
}
|
|
381
|
+
//#endregion
|
|
382
|
+
//#region src/client/api.ts
|
|
383
|
+
/**
|
|
384
|
+
* Browser-side API client for the /api/dsh-ssh route family. The only data
|
|
385
|
+
* access path the panel components use — plain fetch/WebSocket, same origin.
|
|
386
|
+
*/
|
|
387
|
+
/** Error carrying the route's JSON error message. */
|
|
388
|
+
var SshApiError = class extends Error {
|
|
389
|
+
constructor(message) {
|
|
390
|
+
super(message);
|
|
391
|
+
this.name = "SshApiError";
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
/** Extract the `.error` message from a JSON error body; undefined when absent. */
|
|
395
|
+
function extractError(text) {
|
|
396
|
+
if (text === "") return void 0;
|
|
397
|
+
try {
|
|
398
|
+
const parsed = JSON.parse(text);
|
|
399
|
+
return typeof parsed?.error === "string" ? parsed.error : void 0;
|
|
400
|
+
} catch {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
/** Parse a JSON response or throw an SshApiError. */
|
|
405
|
+
async function readJson(response) {
|
|
406
|
+
let body;
|
|
407
|
+
try {
|
|
408
|
+
body = await response.json();
|
|
409
|
+
} catch {
|
|
410
|
+
if (response.status === 404) throw new SshApiError(tt("error.disabled"));
|
|
411
|
+
throw new SshApiError(`HTTP ${response.status}: invalid JSON response`);
|
|
412
|
+
}
|
|
413
|
+
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}`);
|
|
414
|
+
return body;
|
|
415
|
+
}
|
|
416
|
+
/** Query-string helper. */
|
|
417
|
+
function query(params) {
|
|
418
|
+
const search = new URLSearchParams();
|
|
419
|
+
for (const [key, value] of Object.entries(params)) if (value !== void 0 && value !== "") search.set(key, String(value));
|
|
420
|
+
const text = search.toString();
|
|
421
|
+
return text === "" ? "" : "?" + text;
|
|
422
|
+
}
|
|
423
|
+
/** The browser half's only data entry point. */
|
|
424
|
+
var SshApi = class {
|
|
425
|
+
async listHosts(queryText) {
|
|
426
|
+
return (await readJson(await fetch(SSH_API.hosts + query({ query: queryText })))).hosts;
|
|
427
|
+
}
|
|
428
|
+
async createHost(payload) {
|
|
429
|
+
return (await readJson(await fetch(SSH_API.hosts, {
|
|
430
|
+
method: "POST",
|
|
431
|
+
headers: { "content-type": "application/json" },
|
|
432
|
+
body: JSON.stringify(payload)
|
|
433
|
+
}))).host;
|
|
434
|
+
}
|
|
435
|
+
async updateHost(alias, patch) {
|
|
436
|
+
return (await readJson(await fetch(SSH_API.hosts + query({ alias }), {
|
|
437
|
+
method: "PATCH",
|
|
438
|
+
headers: { "content-type": "application/json" },
|
|
439
|
+
body: JSON.stringify(patch)
|
|
440
|
+
}))).host;
|
|
441
|
+
}
|
|
442
|
+
async deleteHost(alias) {
|
|
443
|
+
await readJson(await fetch(SSH_API.hosts + query({ alias }), { method: "DELETE" }));
|
|
444
|
+
}
|
|
445
|
+
async importSshConfig() {
|
|
446
|
+
return (await readJson(await fetch(SSH_API.importSshConfig, { method: "POST" }))).result;
|
|
447
|
+
}
|
|
448
|
+
async testHost(alias) {
|
|
449
|
+
return (await readJson(await fetch(SSH_API.test, {
|
|
450
|
+
method: "POST",
|
|
451
|
+
headers: { "content-type": "application/json" },
|
|
452
|
+
body: JSON.stringify({ alias })
|
|
453
|
+
}))).result;
|
|
454
|
+
}
|
|
455
|
+
async exec(alias, command, timeoutMs) {
|
|
456
|
+
return (await readJson(await fetch(SSH_API.exec, {
|
|
457
|
+
method: "POST",
|
|
458
|
+
headers: { "content-type": "application/json" },
|
|
459
|
+
body: JSON.stringify({
|
|
460
|
+
alias,
|
|
461
|
+
command,
|
|
462
|
+
timeoutMs
|
|
463
|
+
})
|
|
464
|
+
}))).result;
|
|
465
|
+
}
|
|
466
|
+
async cluster(options) {
|
|
467
|
+
return (await readJson(await fetch(SSH_API.cluster, {
|
|
468
|
+
method: "POST",
|
|
469
|
+
headers: { "content-type": "application/json" },
|
|
470
|
+
body: JSON.stringify(options)
|
|
471
|
+
}))).results;
|
|
472
|
+
}
|
|
473
|
+
async ls(alias, path) {
|
|
474
|
+
return (await readJson(await fetch(SSH_API.ls + query({
|
|
475
|
+
alias,
|
|
476
|
+
path
|
|
477
|
+
})))).entries;
|
|
478
|
+
}
|
|
479
|
+
async listTunnels() {
|
|
480
|
+
return (await readJson(await fetch(SSH_API.tunnel, {
|
|
481
|
+
method: "POST",
|
|
482
|
+
headers: { "content-type": "application/json" },
|
|
483
|
+
body: JSON.stringify({ action: "list" })
|
|
484
|
+
}))).tunnels;
|
|
485
|
+
}
|
|
486
|
+
async startTunnel(options) {
|
|
487
|
+
return (await readJson(await fetch(SSH_API.tunnel, {
|
|
488
|
+
method: "POST",
|
|
489
|
+
headers: { "content-type": "application/json" },
|
|
490
|
+
body: JSON.stringify({
|
|
491
|
+
action: "start",
|
|
492
|
+
...options
|
|
493
|
+
})
|
|
494
|
+
}))).tunnel;
|
|
495
|
+
}
|
|
496
|
+
async stopTunnel(tunnelId) {
|
|
497
|
+
return (await readJson(await fetch(SSH_API.tunnel, {
|
|
498
|
+
method: "POST",
|
|
499
|
+
headers: { "content-type": "application/json" },
|
|
500
|
+
body: JSON.stringify({
|
|
501
|
+
action: "stop",
|
|
502
|
+
tunnelId
|
|
503
|
+
})
|
|
504
|
+
}))).ok;
|
|
505
|
+
}
|
|
506
|
+
async stopAllTunnels(alias) {
|
|
507
|
+
return (await readJson(await fetch(SSH_API.tunnel, {
|
|
508
|
+
method: "POST",
|
|
509
|
+
headers: { "content-type": "application/json" },
|
|
510
|
+
body: JSON.stringify({
|
|
511
|
+
action: "stop-all",
|
|
512
|
+
alias
|
|
513
|
+
})
|
|
514
|
+
}))).stopped;
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Upload one file (raw bytes) to a remote path. Progress arrives through
|
|
518
|
+
* the NDJSON response stream; resolves when the result frame lands.
|
|
519
|
+
*/
|
|
520
|
+
async uploadFile(file, alias, remotePath, onProgress) {
|
|
521
|
+
const response = await fetch(SSH_API.upload + query({
|
|
522
|
+
alias,
|
|
523
|
+
remotePath
|
|
524
|
+
}), {
|
|
525
|
+
method: "POST",
|
|
526
|
+
body: file
|
|
527
|
+
});
|
|
528
|
+
if (!response.ok || response.body === null) throw new SshApiError(extractError(await response.text().catch(() => "")) ?? `upload failed: HTTP ${response.status}`);
|
|
529
|
+
const reader = response.body.getReader();
|
|
530
|
+
const decoder = new TextDecoder();
|
|
531
|
+
let buffer = "";
|
|
532
|
+
let finalError;
|
|
533
|
+
let sawResult = false;
|
|
534
|
+
let transferredBytes = 0;
|
|
535
|
+
for (;;) {
|
|
536
|
+
const { done, value } = await reader.read();
|
|
537
|
+
if (done) break;
|
|
538
|
+
buffer += decoder.decode(value, { stream: true });
|
|
539
|
+
const lines = buffer.split("\n");
|
|
540
|
+
buffer = lines.pop() ?? "";
|
|
541
|
+
for (const line of lines) {
|
|
542
|
+
if (line.trim() === "") continue;
|
|
543
|
+
let parsed;
|
|
544
|
+
try {
|
|
545
|
+
parsed = JSON.parse(line);
|
|
546
|
+
} catch {
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
if (parsed.type === "progress") onProgress?.(parsed.progress);
|
|
550
|
+
else if (parsed.type === "result") {
|
|
551
|
+
sawResult = true;
|
|
552
|
+
if (parsed.ok) transferredBytes = parsed.transferredBytes ?? 0;
|
|
553
|
+
finalError = parsed.ok ? void 0 : parsed.error ?? "upload failed";
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
if (finalError !== void 0) throw new SshApiError(finalError);
|
|
558
|
+
if (!sawResult) throw new SshApiError("upload ended without a result frame — the transfer did not complete");
|
|
559
|
+
return { transferredBytes };
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Download a remote file with client-side progress. Streams straight to
|
|
563
|
+
* disk when the File System Access API is available (no full-file RAM
|
|
564
|
+
* copy); otherwise falls back to an in-memory Blob.
|
|
565
|
+
*/
|
|
566
|
+
async downloadFile(alias, remotePath, onProgress) {
|
|
567
|
+
const response = await fetch(SSH_API.download + query({
|
|
568
|
+
alias,
|
|
569
|
+
remotePath
|
|
570
|
+
}));
|
|
571
|
+
if (!response.ok || response.body === null) throw new SshApiError(extractError(await response.text().catch(() => "")) ?? `download failed: HTTP ${response.status}`);
|
|
572
|
+
const total = Number(response.headers.get("content-length") ?? "0");
|
|
573
|
+
const disposition = response.headers.get("content-disposition") ?? "";
|
|
574
|
+
const filename = /filename="([^"]+)"/.exec(disposition)?.[1] ?? remotePath.split("/").pop() ?? "download";
|
|
575
|
+
const reader = response.body.getReader();
|
|
576
|
+
const picker = typeof window !== "undefined" ? window.showSaveFilePicker : void 0;
|
|
577
|
+
let streamed = false;
|
|
578
|
+
let writable;
|
|
579
|
+
const chunks = [];
|
|
580
|
+
let received = 0;
|
|
581
|
+
const progress = () => {
|
|
582
|
+
onProgress?.({
|
|
583
|
+
phase: "transferring",
|
|
584
|
+
file: remotePath,
|
|
585
|
+
transferred: received,
|
|
586
|
+
total,
|
|
587
|
+
percent: total > 0 ? Math.round(received / total * 1e3) / 10 : 0
|
|
588
|
+
});
|
|
589
|
+
};
|
|
590
|
+
try {
|
|
591
|
+
if (picker !== void 0) {
|
|
592
|
+
writable = await (await picker.call(window, { suggestedName: filename })).createWritable();
|
|
593
|
+
streamed = true;
|
|
594
|
+
}
|
|
595
|
+
} catch {}
|
|
596
|
+
for (;;) {
|
|
597
|
+
const { done, value } = await reader.read();
|
|
598
|
+
if (done) break;
|
|
599
|
+
if (writable !== void 0) await writable.write(value);
|
|
600
|
+
else chunks.push(value);
|
|
601
|
+
received += value.length;
|
|
602
|
+
progress();
|
|
603
|
+
}
|
|
604
|
+
if (writable !== void 0) await writable.close();
|
|
605
|
+
onProgress?.({
|
|
606
|
+
phase: "done",
|
|
607
|
+
file: remotePath,
|
|
608
|
+
transferred: received,
|
|
609
|
+
total: received > 0 ? received : total,
|
|
610
|
+
percent: 100
|
|
611
|
+
});
|
|
612
|
+
return {
|
|
613
|
+
blob: streamed ? void 0 : new Blob(chunks),
|
|
614
|
+
filename,
|
|
615
|
+
streamed,
|
|
616
|
+
bytes: received
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
/** Open a WebSocket terminal session. */
|
|
620
|
+
openTerminal(alias, cols, rows) {
|
|
621
|
+
const url = (window.location.protocol === "https:" ? "wss" : "ws") + "://" + window.location.host + SSH_API.terminal + query({
|
|
622
|
+
alias,
|
|
623
|
+
cols,
|
|
624
|
+
rows
|
|
625
|
+
});
|
|
626
|
+
const socket = new WebSocket(url);
|
|
627
|
+
const connection = {
|
|
628
|
+
onReady: void 0,
|
|
629
|
+
onOutput: void 0,
|
|
630
|
+
onExit: void 0,
|
|
631
|
+
onAuthPrompt: void 0,
|
|
632
|
+
send: (data) => {
|
|
633
|
+
if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify({
|
|
634
|
+
type: "input",
|
|
635
|
+
data
|
|
636
|
+
}));
|
|
637
|
+
},
|
|
638
|
+
resize: (cols, rows) => {
|
|
639
|
+
if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify({
|
|
640
|
+
type: "resize",
|
|
641
|
+
cols,
|
|
642
|
+
rows
|
|
643
|
+
}));
|
|
644
|
+
},
|
|
645
|
+
sendAuthResponse: (responses) => {
|
|
646
|
+
if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify({
|
|
647
|
+
type: "auth_response",
|
|
648
|
+
responses
|
|
649
|
+
}));
|
|
650
|
+
},
|
|
651
|
+
close: () => {
|
|
652
|
+
try {
|
|
653
|
+
socket.close();
|
|
654
|
+
} catch {}
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
socket.onmessage = (event) => {
|
|
658
|
+
let frame;
|
|
659
|
+
try {
|
|
660
|
+
frame = JSON.parse(event.data);
|
|
661
|
+
} catch {
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
if (frame.type === "ready") connection.onReady?.();
|
|
665
|
+
else if (frame.type === "output") connection.onOutput?.(frame.data);
|
|
666
|
+
else if (frame.type === "exit") connection.onExit?.(frame.code, frame.error);
|
|
667
|
+
else if (frame.type === "auth_prompt") connection.onAuthPrompt?.(frame.name, frame.instructions, frame.prompts);
|
|
668
|
+
};
|
|
669
|
+
socket.onclose = () => {
|
|
670
|
+
connection.onExit?.(null, "connection closed");
|
|
671
|
+
};
|
|
672
|
+
socket.onerror = () => {
|
|
673
|
+
connection.onExit?.(null, "connection error");
|
|
674
|
+
};
|
|
675
|
+
return connection;
|
|
676
|
+
}
|
|
677
|
+
};
|
|
657
678
|
//#endregion
|
|
658
679
|
//#region \0dsh-css:packages/dsh-ssh/src/client/panel/panel.module.css.mjs
|
|
659
680
|
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}";
|
|
@@ -14786,6 +14807,8 @@ window.__ModuleLoader__.load({
|
|
|
14786
14807
|
const [hosts, setHosts] = (0, react.useState)([]);
|
|
14787
14808
|
const [alias, setAlias] = (0, react.useState)(presetAlias ?? "");
|
|
14788
14809
|
const [status, setStatus] = (0, react.useState)({ kind: "idle" });
|
|
14810
|
+
const [authPrompt, setAuthPrompt] = (0, react.useState)(void 0);
|
|
14811
|
+
const [authInputs, setAuthInputs] = (0, react.useState)([]);
|
|
14789
14812
|
const containerRef = (0, react.useRef)(null);
|
|
14790
14813
|
const termRef = (0, react.useRef)(null);
|
|
14791
14814
|
const fitRef = (0, react.useRef)(null);
|
|
@@ -14826,12 +14849,15 @@ window.__ModuleLoader__.load({
|
|
|
14826
14849
|
if (presetAlias !== void 0) setAlias(presetAlias);
|
|
14827
14850
|
}, [presetAlias, requestId]);
|
|
14828
14851
|
const teardown = () => {
|
|
14852
|
+
setAuthPrompt(void 0);
|
|
14853
|
+
setAuthInputs([]);
|
|
14829
14854
|
const connection = connRef.current;
|
|
14830
14855
|
connRef.current = null;
|
|
14831
14856
|
if (connection !== null) {
|
|
14832
14857
|
connection.onReady = void 0;
|
|
14833
14858
|
connection.onOutput = void 0;
|
|
14834
14859
|
connection.onExit = void 0;
|
|
14860
|
+
connection.onAuthPrompt = void 0;
|
|
14835
14861
|
connection.close();
|
|
14836
14862
|
}
|
|
14837
14863
|
dataSubRef.current?.dispose();
|
|
@@ -14902,7 +14928,17 @@ window.__ModuleLoader__.load({
|
|
|
14902
14928
|
dataSubRef.current = term.onData((data) => {
|
|
14903
14929
|
connection.send(data);
|
|
14904
14930
|
});
|
|
14931
|
+
connection.onAuthPrompt = (name, instructions, prompts) => {
|
|
14932
|
+
setAuthPrompt({
|
|
14933
|
+
name,
|
|
14934
|
+
instructions,
|
|
14935
|
+
prompts
|
|
14936
|
+
});
|
|
14937
|
+
setAuthInputs(prompts.map(() => ""));
|
|
14938
|
+
};
|
|
14905
14939
|
connection.onReady = () => {
|
|
14940
|
+
setAuthPrompt(void 0);
|
|
14941
|
+
setAuthInputs([]);
|
|
14906
14942
|
setStatus({
|
|
14907
14943
|
kind: "connected",
|
|
14908
14944
|
alias: target
|
|
@@ -14914,6 +14950,8 @@ window.__ModuleLoader__.load({
|
|
|
14914
14950
|
connection.onExit = (_code, error) => {
|
|
14915
14951
|
if (settled) return;
|
|
14916
14952
|
settled = true;
|
|
14953
|
+
setAuthPrompt(void 0);
|
|
14954
|
+
setAuthInputs([]);
|
|
14917
14955
|
dataSubRef.current?.dispose();
|
|
14918
14956
|
dataSubRef.current = null;
|
|
14919
14957
|
term.options.disableStdin = true;
|
|
@@ -14929,6 +14967,12 @@ window.__ModuleLoader__.load({
|
|
|
14929
14967
|
teardown();
|
|
14930
14968
|
setStatus({ kind: "idle" });
|
|
14931
14969
|
};
|
|
14970
|
+
const submitAuth = (e) => {
|
|
14971
|
+
e?.preventDefault();
|
|
14972
|
+
if (connRef.current === null || authPrompt === void 0) return;
|
|
14973
|
+
connRef.current.sendAuthResponse(authInputs);
|
|
14974
|
+
setAuthPrompt(void 0);
|
|
14975
|
+
};
|
|
14932
14976
|
const active = status.kind === "connecting" || status.kind === "connected";
|
|
14933
14977
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
14934
14978
|
className: panel_module_css_default.termBody,
|
|
@@ -14993,14 +15037,73 @@ window.__ModuleLoader__.load({
|
|
|
14993
15037
|
}),
|
|
14994
15038
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
14995
15039
|
className: panel_module_css_default.termWrap,
|
|
14996
|
-
children: [
|
|
14997
|
-
|
|
14998
|
-
|
|
14999
|
-
|
|
15000
|
-
|
|
15001
|
-
|
|
15002
|
-
|
|
15003
|
-
|
|
15040
|
+
children: [
|
|
15041
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
15042
|
+
ref: containerRef,
|
|
15043
|
+
className: panel_module_css_default.termContainer,
|
|
15044
|
+
"data-dsh-part": "terminal"
|
|
15045
|
+
}),
|
|
15046
|
+
status.kind === "idle" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
15047
|
+
className: panel_module_css_default.termPlaceholder,
|
|
15048
|
+
children: hosts.length === 0 ? tt("hosts.empty") : tt("terminal.placeholder")
|
|
15049
|
+
}),
|
|
15050
|
+
authPrompt !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
15051
|
+
className: panel_module_css_default.modalBackdrop,
|
|
15052
|
+
style: {
|
|
15053
|
+
position: "absolute",
|
|
15054
|
+
zIndex: 10
|
|
15055
|
+
},
|
|
15056
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
|
|
15057
|
+
className: panel_module_css_default.modalCard,
|
|
15058
|
+
style: { maxWidth: 420 },
|
|
15059
|
+
onSubmit: submitAuth,
|
|
15060
|
+
children: [
|
|
15061
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
15062
|
+
className: panel_module_css_default.modalHeader,
|
|
15063
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
15064
|
+
className: panel_module_css_default.modalTitle,
|
|
15065
|
+
children: authPrompt.name || tt("terminal.auth.title")
|
|
15066
|
+
})
|
|
15067
|
+
}),
|
|
15068
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
15069
|
+
className: panel_module_css_default.hint,
|
|
15070
|
+
children: authPrompt.instructions || tt("terminal.auth.hint")
|
|
15071
|
+
}),
|
|
15072
|
+
authPrompt.prompts.map((p, idx) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
15073
|
+
className: panel_module_css_default.field,
|
|
15074
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
15075
|
+
className: panel_module_css_default.fieldLabel,
|
|
15076
|
+
children: p.prompt.trim() || tt("terminal.auth.title")
|
|
15077
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
15078
|
+
autoFocus: idx === 0,
|
|
15079
|
+
className: panel_module_css_default.input,
|
|
15080
|
+
type: p.echo ? "text" : "password",
|
|
15081
|
+
placeholder: tt("terminal.auth.placeholder"),
|
|
15082
|
+
value: authInputs[idx] ?? "",
|
|
15083
|
+
onChange: (event) => {
|
|
15084
|
+
const next = [...authInputs];
|
|
15085
|
+
next[idx] = event.target.value;
|
|
15086
|
+
setAuthInputs(next);
|
|
15087
|
+
}
|
|
15088
|
+
})]
|
|
15089
|
+
}, idx)),
|
|
15090
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
15091
|
+
className: panel_module_css_default.modalFooter,
|
|
15092
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
15093
|
+
type: "button",
|
|
15094
|
+
className: panel_module_css_default.ghostButton,
|
|
15095
|
+
onClick: disconnect,
|
|
15096
|
+
children: tt("terminal.auth.cancel")
|
|
15097
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
15098
|
+
type: "submit",
|
|
15099
|
+
className: panel_module_css_default.primaryButton,
|
|
15100
|
+
children: tt("terminal.auth.submit")
|
|
15101
|
+
})]
|
|
15102
|
+
})
|
|
15103
|
+
]
|
|
15104
|
+
})
|
|
15105
|
+
})
|
|
15106
|
+
]
|
|
15004
15107
|
})
|
|
15005
15108
|
]
|
|
15006
15109
|
});
|
|
@@ -15982,7 +16085,7 @@ window.__ModuleLoader__.load({
|
|
|
15982
16085
|
/** The building package's version, when the bundle carries it. */
|
|
15983
16086
|
function bakedVersion() {
|
|
15984
16087
|
try {
|
|
15985
|
-
return "0.3.
|
|
16088
|
+
return "0.3.9";
|
|
15986
16089
|
} catch {
|
|
15987
16090
|
return;
|
|
15988
16091
|
}
|