dsh-remote-workspaces 0.2.1 → 0.3.1
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 +7723 -0
- package/package.json +11 -3
- package/src/client.js +540 -23
- package/src/index.js +126 -2
- package/src/local-browse.js +168 -0
- package/src/registry.js +20 -11
- package/src/routing-fs.js +54 -7
- package/src/shell-sessions.js +180 -0
- package/src/transport.js +73 -0
package/src/transport.js
CHANGED
|
@@ -29,6 +29,22 @@ export function psQuote(value) {
|
|
|
29
29
|
return `'${String(value).replace(/'/g, "''")}'`
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* First-input `cd` command that lands an interactive shell channel in `cwd`,
|
|
34
|
+
* dialect-aware: POSIX single-quotes, PowerShell (cd = Set-Location) single-
|
|
35
|
+
* quotes a forward-slash win path, and cmd.exe — where single quotes are
|
|
36
|
+
* literal — uses double quotes + backslashes with `/d` to switch drive too.
|
|
37
|
+
*/
|
|
38
|
+
export function shellCdCommand(profile, cwd) {
|
|
39
|
+
const p = profile ?? { family: 'unknown' }
|
|
40
|
+
if (p.family === 'windows') {
|
|
41
|
+
const winPath = toWinPath(cwd)
|
|
42
|
+
if (p.shell === 'cmd') return `cd /d "${winPath.replace(/\//g, '\\')}"\r`
|
|
43
|
+
return `cd ${psQuote(winPath)}\r`
|
|
44
|
+
}
|
|
45
|
+
return `cd ${shellQuote(cwd)}\r`
|
|
46
|
+
}
|
|
47
|
+
|
|
32
48
|
/**
|
|
33
49
|
* Strip PowerShell progress records from a stderr capture. When a nested
|
|
34
50
|
* powershell is launched by a Windows OpenSSH exec channel (e.g. the cmd
|
|
@@ -497,6 +513,63 @@ export class SshClient {
|
|
|
497
513
|
})
|
|
498
514
|
}
|
|
499
515
|
|
|
516
|
+
/**
|
|
517
|
+
* Open an INTERACTIVE shell channel with a PTY — the remote half of the UI
|
|
518
|
+
* Shell tool. ssh2 `conn.shell` + a pty is the same mechanism on POSIX and
|
|
519
|
+
* Windows OpenSSH (ConPTY) remotes; the target's DefaultShell decides whether
|
|
520
|
+
* the session is a login shell, cmd, or PowerShell, and its OS decides the
|
|
521
|
+
* line endings. The channel merges stderr into stdout (one data stream) and
|
|
522
|
+
* the bytes are passed through RAW — a PTY already emits terminal-ready CRLF
|
|
523
|
+
* that xterm renders directly, so the exec-channel CRLF folding must NOT be
|
|
524
|
+
* applied here.
|
|
525
|
+
*
|
|
526
|
+
* When `cwd` is given, the shell is chdir'd there as its first input (a
|
|
527
|
+
* dialect-aware `cd`, probed once per target). The shell still starts at the
|
|
528
|
+
* remote account's home; the `cd` is emitted into the PTY so the prompt and
|
|
529
|
+
* every later command land in the requested directory.
|
|
530
|
+
*
|
|
531
|
+
* Resolves a handle compatible with the local PTY one:
|
|
532
|
+
* { output, write, terminate, resize, pid } where `output` is the ssh2
|
|
533
|
+
* stream (an EventEmitter emitting 'data' and 'close'), `resize` maps to
|
|
534
|
+
* `setWindow` (REACHABLE here, unlike the local seam — the S0 finding), and
|
|
535
|
+
* `terminate` closes the channel then ends the connection.
|
|
536
|
+
*/
|
|
537
|
+
openShell({ rows = 24, cols = 80, term = 'xterm-256color', env, cwd } = {}) {
|
|
538
|
+
const chdirPromise = cwd !== undefined && cwd !== null && cwd !== ''
|
|
539
|
+
? this.profile().then((profile) => shellCdCommand(profile, cwd)).catch(() => '')
|
|
540
|
+
: Promise.resolve('')
|
|
541
|
+
return chdirPromise.then((chdir) => new Promise((resolve, reject) => {
|
|
542
|
+
const conn = new Client()
|
|
543
|
+
let settled = false
|
|
544
|
+
const fail = (error) => {
|
|
545
|
+
if (settled) return
|
|
546
|
+
settled = true
|
|
547
|
+
try { conn.end() } catch {}
|
|
548
|
+
reject(error instanceof Error ? error : new Error(String(error)))
|
|
549
|
+
}
|
|
550
|
+
const timer = setTimeout(() => fail(new Error('SSH shell 连接超时')), this.readyTimeoutMs)
|
|
551
|
+
conn.on('ready', () => {
|
|
552
|
+
conn.shell({ term, rows, cols, ...(env ? { env } : {}) }, (err, stream) => {
|
|
553
|
+
if (err) { clearTimeout(timer); fail(err); return }
|
|
554
|
+
clearTimeout(timer)
|
|
555
|
+
settled = true
|
|
556
|
+
let closed = false
|
|
557
|
+
stream.on('close', () => { closed = true })
|
|
558
|
+
if (chdir !== '') stream.write(chdir)
|
|
559
|
+
resolve({
|
|
560
|
+
output: stream,
|
|
561
|
+
pid: null,
|
|
562
|
+
write(data) { if (!closed) { try { stream.write(data) } catch {} } },
|
|
563
|
+
terminate() { try { stream.close() } catch {} try { conn.end() } catch {} },
|
|
564
|
+
resize(r, c) { if (!closed) { try { stream.setWindow(r, c) } catch {} } },
|
|
565
|
+
})
|
|
566
|
+
})
|
|
567
|
+
})
|
|
568
|
+
conn.on('error', (connError) => { clearTimeout(timer); fail(connError) })
|
|
569
|
+
try { conn.connect(this.connectConfig()) } catch (connectError) { clearTimeout(timer); fail(connectError) }
|
|
570
|
+
}))
|
|
571
|
+
}
|
|
572
|
+
|
|
500
573
|
/**
|
|
501
574
|
* Open a LONG-LIVED exec channel for streaming (used by background/start
|
|
502
575
|
* jobs on Windows remotes, where no nohup-style detach exists and closing
|