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/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