dsh-remote 0.8.72 → 0.8.74

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.
Files changed (44) hide show
  1. package/LICENSE +21 -21
  2. package/README.en.md +470 -389
  3. package/README.md +529 -462
  4. package/cordis.patch.yml +9 -9
  5. package/lib/binding.js +172 -172
  6. package/lib/client.js +4039 -4022
  7. package/lib/credential.js +263 -263
  8. package/lib/errors.js +141 -141
  9. package/lib/exec-job.js +235 -235
  10. package/lib/file-reference.js +457 -457
  11. package/lib/forwards.js +258 -258
  12. package/lib/host-api.js +808 -0
  13. package/lib/hostkey.js +108 -108
  14. package/lib/http-transport.js +196 -196
  15. package/lib/ignore.js +202 -202
  16. package/lib/index.js +4923 -4541
  17. package/lib/instructions-dynamic.js +415 -415
  18. package/lib/openssh.js +879 -879
  19. package/lib/paths.js +252 -252
  20. package/lib/pool.js +963 -893
  21. package/lib/registry.js +296 -293
  22. package/lib/remote-fs.js +173 -173
  23. package/lib/remote-sandbox.js +183 -183
  24. package/lib/remote-shell-child.cjs +344 -305
  25. package/lib/remote-shell.js +542 -518
  26. package/lib/routes-fs.js +245 -245
  27. package/lib/routes-term.js +169 -169
  28. package/lib/search.js +235 -235
  29. package/lib/sftp-surface.js +119 -119
  30. package/lib/sshconfig.js +779 -705
  31. package/lib/sync.js +275 -275
  32. package/lib/tasks.js +95 -95
  33. package/lib/telemetry.js +213 -213
  34. package/lib/terminal.js +205 -205
  35. package/lib/tui/flow.js +130 -0
  36. package/lib/tui/format.js +332 -0
  37. package/lib/tui/i18n.js +344 -0
  38. package/lib/tui/index.js +322 -0
  39. package/lib/tui/provider.js +327 -0
  40. package/lib/tui/scene.js +996 -0
  41. package/lib/update.js +472 -472
  42. package/lib/web-attach.js +850 -850
  43. package/lib/workspace-cleanup.js +337 -0
  44. package/package.json +100 -99
@@ -1,119 +1,119 @@
1
- // dsh-remote — the promisified SFTP surface every backend presents.
2
- //
3
- // Both SSH transports (the native ssh2 pool in lib/pool.js and the
4
- // system-OpenSSH pool in lib/openssh.js) must expose the SAME object to
5
- // callers: lib/sync.js, lib/search.js, lib/routes-fs.js, lib/remote-fs.js and
6
- // the rw_* tools all consume this exact shape. Keeping one implementation
7
- // matters more than it looks: the timeout, dead-channel and range-read rules
8
- // below encode failures that were expensive to find (issue #44), and a second
9
- // hand-copied version would drift away from the fixed one.
10
- //
11
- // The two rules that are easy to get wrong:
12
- // • A dead channel must fail FAST. ssh2's own EOF handling only rejects the
13
- // requests pending at that moment; every later request is registered and
14
- // never answered, so each waits out the full commandTimeoutMs. Over a tree
15
- // walk that is N × 2 × timeout — days on a large directory.
16
- // • `readPartial` exists so an oversized preview never downloads the whole
17
- // file to show its head.
18
- import { toSftpPath } from './paths.js'
19
-
20
- /**
21
- * Wrap a live SFTP channel (ssh2's SFTP instance) in the promisified surface
22
- * the plugin's callers expect.
23
- *
24
- * @param {object} sftp - an ssh2 SFTP channel (already negotiated/ready).
25
- * @param {object} opts
26
- * @param {number} opts.commandTimeoutMs - per-operation timeout.
27
- * @returns {object} promisified sftp surface.
28
- */
29
- export function buildSftpSurface(sftp, { commandTimeoutMs, onTimeout }) {
30
- // A dead channel must fail fast (see the header). ssh2's SFTP instance and the
31
- // system-OpenSSH engine both emit these, so one hook covers both backends.
32
- let dead = false
33
- const markDead = () => { dead = true }
34
- if (typeof sftp.on === 'function') {
35
- sftp.on('end', markDead)
36
- sftp.on('close', markDead)
37
- sftp.on('error', markDead)
38
- }
39
- const withTimeout = (fn) => (...args) =>
40
- new Promise((resolve, reject) => {
41
- if (dead) return reject(new Error('sftp channel closed'))
42
- let timedOut = false
43
- const timer = setTimeout(() => {
44
- timedOut = true
45
- markDead()
46
- if (typeof onTimeout === 'function') { try { onTimeout() } catch { /* best effort */ } }
47
- reject(new Error('sftp operation timed out'))
48
- }, commandTimeoutMs)
49
- const finish = (err, value) => {
50
- clearTimeout(timer)
51
- if (timedOut) return
52
- err ? reject(err) : resolve(value)
53
- }
54
- try { fn(...args, finish) } catch (err) { clearTimeout(timer); if (!timedOut) reject(err) }
55
- })
56
- const P = (p) => toSftpPath(p)
57
- return {
58
- readdir: (dir) => withTimeout((d, cb) => sftp.readdir(d, cb))(P(dir)),
59
- stat: (p) => withTimeout((d, cb) => sftp.stat(d, cb))(P(p)),
60
- lstat: (p) => withTimeout((d, cb) => sftp.lstat(d, cb))(P(p)),
61
- mkdir: (dir) => withTimeout((d, cb) => sftp.mkdir(d, cb))(P(dir)),
62
- rmdir: (dir) => withTimeout((d, cb) => sftp.rmdir(d, cb))(P(dir)),
63
- unlink: (p) => withTimeout((d, cb) => sftp.unlink(d, cb))(P(p)),
64
- rename: (p, d) => withTimeout((a, b, cb) => sftp.rename(a, b, cb))(P(p), P(d)),
65
- // POSIX rename(2) semantics: atomically REPLACE an existing destination.
66
- // Plain SSH_FXP_RENAME fails when the target exists (OpenSSH's sftp-server
67
- // enforces that even though it advertises posix-rename@openssh.com — ssh2
68
- // only sends the extension when asked explicitly, verified against
69
- // OpenSSH_9.6 in issue #65). Servers with the extension get one atomic
70
- // request; servers without it get the move-aside dance, whose every hop
71
- // targets a path that does not exist, which plain RENAME always permits.
72
- // rw_edit's atomic write depends on this — without it every edit of an
73
- // existing file fails with a bare "Failure".
74
- renameOverwrite: (p, d) => withTimeout((a, b, cb) => {
75
- const ext = sftp._extensions && sftp._extensions['posix-rename@openssh.com']
76
- if (ext === '1' && typeof sftp.ext_openssh_rename === 'function') {
77
- return sftp.ext_openssh_rename(a, b, cb)
78
- }
79
- const bak = `${b}.dsh-remote-bak-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.tmp`
80
- sftp.rename(b, bak, (e1) => {
81
- if (e1) {
82
- // Destination absent → a plain rename is already overwrite-safe.
83
- if (/no such/i.test(String((e1 && e1.message) || e1))) return sftp.rename(a, b, cb)
84
- return cb(e1)
85
- }
86
- sftp.rename(a, b, (e2) => {
87
- if (e2) {
88
- // Roll the original back into place before reporting the failure.
89
- return sftp.rename(bak, b, () => cb(e2))
90
- }
91
- // Best effort: a leftover backup must not fail the operation.
92
- sftp.unlink(bak, () => cb(null))
93
- })
94
- })
95
- })(P(p), P(d)),
96
- realpath: (p) => withTimeout((d, cb) => sftp.realpath(d, cb))(P(p)),
97
- // Attribute write (chmod semantics live in setstat at the protocol level,
98
- // so BOTH backends — ssh2's SFTPWrapper and its raw protocol SFTP — answer
99
- // this one shape). Used by rw_edit to restore the original mode after an
100
- // atomic rename replace (issue #65).
101
- setstat: (p, attrs) => withTimeout((d, a, cb) => sftp.setstat(d, a, cb))(P(p), attrs),
102
- readFile: (p) => withTimeout((d, cb) => sftp.readFile(d, cb))(P(p)),
103
- writeFile: (p, data) => withTimeout((d, data2, cb) => sftp.writeFile(d, data2, cb))(P(p), data),
104
- fastGet: (p, lp) => withTimeout((d, l, cb) => sftp.fastGet(d, l, cb))(P(p), lp),
105
- fastPut: (lp, p) => withTimeout((l, d, cb) => sftp.fastPut(l, d, cb))(lp, P(p)),
106
- // Range read for oversized previews: never downloads the whole file.
107
- readPartial: (p, offset, length) => withTimeout((d, off, len, cb) => {
108
- sftp.open(d, 'r', (e, handle) => {
109
- if (e) return cb(e)
110
- const buf = Buffer.alloc(len)
111
- sftp.read(handle, buf, 0, len, off, (e2, bytesRead, data) => {
112
- sftp.close(handle, () => {})
113
- if (e2) return cb(e2)
114
- cb(null, Buffer.isBuffer(data) ? data.subarray(0, bytesRead) : buf.subarray(0, bytesRead))
115
- })
116
- })
117
- })(P(p), offset, length),
118
- }
119
- }
1
+ // dsh-remote — the promisified SFTP surface every backend presents.
2
+ //
3
+ // Both SSH transports (the native ssh2 pool in lib/pool.js and the
4
+ // system-OpenSSH pool in lib/openssh.js) must expose the SAME object to
5
+ // callers: lib/sync.js, lib/search.js, lib/routes-fs.js, lib/remote-fs.js and
6
+ // the rw_* tools all consume this exact shape. Keeping one implementation
7
+ // matters more than it looks: the timeout, dead-channel and range-read rules
8
+ // below encode failures that were expensive to find (issue #44), and a second
9
+ // hand-copied version would drift away from the fixed one.
10
+ //
11
+ // The two rules that are easy to get wrong:
12
+ // • A dead channel must fail FAST. ssh2's own EOF handling only rejects the
13
+ // requests pending at that moment; every later request is registered and
14
+ // never answered, so each waits out the full commandTimeoutMs. Over a tree
15
+ // walk that is N × 2 × timeout — days on a large directory.
16
+ // • `readPartial` exists so an oversized preview never downloads the whole
17
+ // file to show its head.
18
+ import { toSftpPath } from './paths.js'
19
+
20
+ /**
21
+ * Wrap a live SFTP channel (ssh2's SFTP instance) in the promisified surface
22
+ * the plugin's callers expect.
23
+ *
24
+ * @param {object} sftp - an ssh2 SFTP channel (already negotiated/ready).
25
+ * @param {object} opts
26
+ * @param {number} opts.commandTimeoutMs - per-operation timeout.
27
+ * @returns {object} promisified sftp surface.
28
+ */
29
+ export function buildSftpSurface(sftp, { commandTimeoutMs, onTimeout }) {
30
+ // A dead channel must fail fast (see the header). ssh2's SFTP instance and the
31
+ // system-OpenSSH engine both emit these, so one hook covers both backends.
32
+ let dead = false
33
+ const markDead = () => { dead = true }
34
+ if (typeof sftp.on === 'function') {
35
+ sftp.on('end', markDead)
36
+ sftp.on('close', markDead)
37
+ sftp.on('error', markDead)
38
+ }
39
+ const withTimeout = (fn) => (...args) =>
40
+ new Promise((resolve, reject) => {
41
+ if (dead) return reject(new Error('sftp channel closed'))
42
+ let timedOut = false
43
+ const timer = setTimeout(() => {
44
+ timedOut = true
45
+ markDead()
46
+ if (typeof onTimeout === 'function') { try { onTimeout() } catch { /* best effort */ } }
47
+ reject(new Error('sftp operation timed out'))
48
+ }, commandTimeoutMs)
49
+ const finish = (err, value) => {
50
+ clearTimeout(timer)
51
+ if (timedOut) return
52
+ err ? reject(err) : resolve(value)
53
+ }
54
+ try { fn(...args, finish) } catch (err) { clearTimeout(timer); if (!timedOut) reject(err) }
55
+ })
56
+ const P = (p) => toSftpPath(p)
57
+ return {
58
+ readdir: (dir) => withTimeout((d, cb) => sftp.readdir(d, cb))(P(dir)),
59
+ stat: (p) => withTimeout((d, cb) => sftp.stat(d, cb))(P(p)),
60
+ lstat: (p) => withTimeout((d, cb) => sftp.lstat(d, cb))(P(p)),
61
+ mkdir: (dir) => withTimeout((d, cb) => sftp.mkdir(d, cb))(P(dir)),
62
+ rmdir: (dir) => withTimeout((d, cb) => sftp.rmdir(d, cb))(P(dir)),
63
+ unlink: (p) => withTimeout((d, cb) => sftp.unlink(d, cb))(P(p)),
64
+ rename: (p, d) => withTimeout((a, b, cb) => sftp.rename(a, b, cb))(P(p), P(d)),
65
+ // POSIX rename(2) semantics: atomically REPLACE an existing destination.
66
+ // Plain SSH_FXP_RENAME fails when the target exists (OpenSSH's sftp-server
67
+ // enforces that even though it advertises posix-rename@openssh.com — ssh2
68
+ // only sends the extension when asked explicitly, verified against
69
+ // OpenSSH_9.6 in issue #65). Servers with the extension get one atomic
70
+ // request; servers without it get the move-aside dance, whose every hop
71
+ // targets a path that does not exist, which plain RENAME always permits.
72
+ // rw_edit's atomic write depends on this — without it every edit of an
73
+ // existing file fails with a bare "Failure".
74
+ renameOverwrite: (p, d) => withTimeout((a, b, cb) => {
75
+ const ext = sftp._extensions && sftp._extensions['posix-rename@openssh.com']
76
+ if (ext === '1' && typeof sftp.ext_openssh_rename === 'function') {
77
+ return sftp.ext_openssh_rename(a, b, cb)
78
+ }
79
+ const bak = `${b}.dsh-remote-bak-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.tmp`
80
+ sftp.rename(b, bak, (e1) => {
81
+ if (e1) {
82
+ // Destination absent → a plain rename is already overwrite-safe.
83
+ if (/no such/i.test(String((e1 && e1.message) || e1))) return sftp.rename(a, b, cb)
84
+ return cb(e1)
85
+ }
86
+ sftp.rename(a, b, (e2) => {
87
+ if (e2) {
88
+ // Roll the original back into place before reporting the failure.
89
+ return sftp.rename(bak, b, () => cb(e2))
90
+ }
91
+ // Best effort: a leftover backup must not fail the operation.
92
+ sftp.unlink(bak, () => cb(null))
93
+ })
94
+ })
95
+ })(P(p), P(d)),
96
+ realpath: (p) => withTimeout((d, cb) => sftp.realpath(d, cb))(P(p)),
97
+ // Attribute write (chmod semantics live in setstat at the protocol level,
98
+ // so BOTH backends — ssh2's SFTPWrapper and its raw protocol SFTP — answer
99
+ // this one shape). Used by rw_edit to restore the original mode after an
100
+ // atomic rename replace (issue #65).
101
+ setstat: (p, attrs) => withTimeout((d, a, cb) => sftp.setstat(d, a, cb))(P(p), attrs),
102
+ readFile: (p) => withTimeout((d, cb) => sftp.readFile(d, cb))(P(p)),
103
+ writeFile: (p, data) => withTimeout((d, data2, cb) => sftp.writeFile(d, data2, cb))(P(p), data),
104
+ fastGet: (p, lp) => withTimeout((d, l, cb) => sftp.fastGet(d, l, cb))(P(p), lp),
105
+ fastPut: (lp, p) => withTimeout((l, d, cb) => sftp.fastPut(l, d, cb))(lp, P(p)),
106
+ // Range read for oversized previews: never downloads the whole file.
107
+ readPartial: (p, offset, length) => withTimeout((d, off, len, cb) => {
108
+ sftp.open(d, 'r', (e, handle) => {
109
+ if (e) return cb(e)
110
+ const buf = Buffer.alloc(len)
111
+ sftp.read(handle, buf, 0, len, off, (e2, bytesRead, data) => {
112
+ sftp.close(handle, () => {})
113
+ if (e2) return cb(e2)
114
+ cb(null, Buffer.isBuffer(data) ? data.subarray(0, bytesRead) : buf.subarray(0, bytesRead))
115
+ })
116
+ })
117
+ })(P(p), offset, length),
118
+ }
119
+ }