dsh-remote 0.8.45 → 0.8.46

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 CHANGED
@@ -3415,26 +3415,40 @@ window.__ModuleLoader__.load({
3415
3415
  const mirror = mirrorForCwd(cwd, MIRRORS.list)
3416
3416
  if (!mirror) return null
3417
3417
  const remoteRoot = String(mirror.remotePath || '').replace(/[\\/]+$/, '')
3418
- const raw = String(parsed.path || '')
3419
- const isWin = raw.includes('\\') || remoteRoot.includes('\\')
3420
- const sep = isWin ? '\\' : '/'
3421
- const norm = (s) => String(s).replace(/[\\/]+/g, sep).replace(new RegExp('\\' + sep + '+$'), '')
3422
- const rel = norm(raw)
3418
+ // The REMOTE host's separator is INDEPENDENT of this machine's, and taking
3419
+ // it from the address was wrong: a Windows client mirroring a Linux host
3420
+ // is the ordinary case here, and reading the separator off the local path
3421
+ // then glued a POSIX root to a Windows join ("/home/u/app" + "\" + "x.c").
3422
+ // Decide from the remote root alone.
3423
+ const remoteIsWin = /^[A-Za-z]:/.test(remoteRoot) || (remoteRoot.includes('\\') && !remoteRoot.includes('/'))
3424
+ const remoteSep = remoteIsWin ? '\\' : '/'
3425
+ const toRemote = (s) => String(s).replace(/[\\/]+/g, remoteSep)
3426
+ // Comparison is separator-INSENSITIVE: `@` completion may insert either
3427
+ // style, and the two sides describe the same directory. The local side
3428
+ // folds case when it looks like a Windows path (Windows paths do).
3429
+ const canon = (s) => String(s).replace(/[\\/]+/g, '/').replace(/\/+$/, '')
3430
+ const localDir = String(mirror.dir || '')
3431
+ const localIsWin = /^[A-Za-z]:/.test(localDir) || localDir.includes('\\')
3432
+ const fold = (s) => (localIsWin ? s.toLowerCase() : s)
3433
+ const rel = canon(parsed.path || '')
3434
+ const mirrorDir = canon(localDir)
3423
3435
  // The mirror's own directory prefix may be baked into the address when the
3424
- // link was written against the local path; strip it so the result is
3425
- // remote-relative rather than duplicated under the remote root.
3426
- const mirrorDir = norm(mirror.dir || '')
3427
- const relBody = rel && mirrorDir && (rel === mirrorDir || rel.startsWith(mirrorDir + sep))
3428
- ? rel.slice(mirrorDir.length).replace(new RegExp('^\\' + sep + '+'), '')
3436
+ // link was written against the LOCAL path (that is what `@` completion
3437
+ // inserts); strip it so the result is remote-relative rather than
3438
+ // duplicated under the remote root.
3439
+ const stripped = rel && mirrorDir && (fold(rel) === fold(mirrorDir) || fold(rel).startsWith(fold(mirrorDir) + '/'))
3440
+ ? rel.slice(mirrorDir.length).replace(/^\/+/, '')
3429
3441
  : rel
3430
- if (!remoteRoot) return relBody || sep
3442
+ const relBody = canon(stripped)
3443
+ if (!remoteRoot) return relBody ? toRemote(relBody) : remoteSep
3431
3444
  if (!relBody) return remoteRoot
3432
- // An absolute remote path that is already inside the remote root is used
3433
- // as-is; anything else absolute is left alone (the reader meant it).
3434
- if (/^[\\/]/.test(relBody) || /^[A-Za-z]:[\\/]/.test(relBody)) {
3435
- return relBody
3436
- }
3437
- return remoteRoot + sep + relBody
3445
+ // A drive-letter absolute path on a POSIX remote is a LOCAL path that fell
3446
+ // outside the mirror: nothing on the remote can name it, so refuse rather
3447
+ // than invent "/home/u/app/D:/x.c".
3448
+ if (!remoteIsWin && /^[A-Za-z]:/.test(relBody)) return null
3449
+ // Genuinely absolute: the reader meant that path on the remote.
3450
+ if (/^[A-Za-z]:/.test(relBody) || /^\//.test(relBody)) return toRemote(relBody)
3451
+ return remoteRoot + remoteSep + toRemote(relBody)
3438
3452
  }
3439
3453
  function NativeExplorer(props) {
3440
3454
  const info = props.useTabInfo()
package/lib/pool.js CHANGED
@@ -31,7 +31,7 @@ export function createPool(config, opts) {
31
31
  *
32
32
  * ssh2 reports a dead connection by THROWING synchronously —
33
33
  * `if (!this._sock || !isWritable(this._sock)) throw new Error('Not connected')`
34
- * (ssh2/lib/client.js:1398-1400) — and never calls the callback, so a
34
+ * (ssh2 >= 1.16, `Client.forwardOut`) — and never calls the callback, so a
35
35
  * callback-only error path cannot guard this call (issue #54). Two further
36
36
  * consequences justify a shared helper rather than an inline try/catch:
37
37
  *
@@ -40,8 +40,11 @@ export function createPool(config, opts) {
40
40
  * • called from a `net.Server` connection handler the throw escapes to the
41
41
  * process top level and kills the host.
42
42
  *
43
- * The system-OpenSSH backend (lib/openssh.js) throws the same way when its
44
- * connection is already closed, so this covers both transports.
43
+ * This guards the CALLER's side of the contract rather than assuming one
44
+ * backend's behaviour: the system-OpenSSH backend (lib/openssh.js `forwardOut`)
45
+ * already converts its own spawn failure into a callback error, so for it the
46
+ * try/catch below is dead code — but a backend is free to throw, and the one
47
+ * that does must not be able to take the process down.
45
48
  *
46
49
  * @param {object} client - an ssh2 Client or OpenSshConnection.
47
50
  * @param {object} spec - `srcHost`/`srcPort`/`dstHost`/`dstPort`.
package/lib/web-attach.js CHANGED
@@ -228,8 +228,12 @@ export class WebAttach {
228
228
  */
229
229
  constructor(opts = {}) {
230
230
  this.net = opts.net || net
231
- this.localPortStart = opts.localPortStart || 3088
232
- this.localPortMax = opts.localPortMax || 50
231
+ // `??`, not `||`: port 0 is a MEANINGFUL value here (let the OS pick), and
232
+ // `0 || 3088` silently turned "let the OS choose" into a bind on a fixed
233
+ // port. An explicit 0 now survives, which is also what lets a test ask for a
234
+ // port without risking a collision with something really listening.
235
+ this.localPortStart = opts.localPortStart ?? 3088
236
+ this.localPortMax = opts.localPortMax ?? 50
233
237
  /** Whether the pool's lifecycle hooks are ours to install. */
234
238
  this.trackPool = opts.trackPool !== false
235
239
  /** @type {import('node:net').Server|null} */ this.server = null
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-remote",
3
- "version": "0.8.45",
3
+ "version": "0.8.46",
4
4
  "description": "Remote-work assistant for DeepSeek Harness: connect SSH (password/key/agent/keyboard-interactive/proxy jump), pick a remote workspace, operate on it with 22 rw_* tools (incl. rw_edit/rw_stat/rw_mkdir/rw_remove/rw_move/rw_forward), conflict-aware mirror sync, one-click remote dsh deploy + web UI attach, port forwarding, optional side-bar remote file editing, audit log, keychain passwords.",
5
5
  "license": "MIT",
6
6
  "author": "flymysql <flyphp@outlook.com>",
@@ -84,6 +84,6 @@
84
84
  "semver": "^7.8.5"
85
85
  },
86
86
  "scripts": {
87
- "test": "node --import ./test/setup-telemetry-off.mjs --test"
87
+ "test": "node --import ./test/setup-telemetry-off.mjs --test --test-timeout=20000 --test-force-exit"
88
88
  }
89
89
  }