dsh-remote 0.8.14 → 0.8.16

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/binding.js CHANGED
@@ -1,97 +1,97 @@
1
- // Session → remote-binding resolution.
2
- //
3
- // A session's remote target is derived from its own workspace mirror, never
4
- // from the plugin's mutable active-machine state. That is what keeps concurrent
5
- // sessions on different hosts independent: with a single shared connection and
6
- // a single shared workspace value, one session switching machines redirected
7
- // every other session's commands to the wrong host (issue #25).
8
- //
9
- // The mirror directory is the durable record of that binding: `ensureMirror()`
10
- // writes `.dsh-remote-meta.json` with the remote origin (`host`/`port`/
11
- // `username`/`remotePath`) when the workspace is picked, so a session's cwd
12
- // alone identifies both the machine to connect to and the workspace root.
13
-
14
- import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
15
- import path from 'node:path'
16
-
17
- /** Strip trailing separators so a mirror root and a path inside it compare cleanly. */
18
- const norm = (p) => path.resolve(String(p || '')).replace(/[\\/]+$/, '') || ''
19
-
20
- /** Whether `base` is `dir` itself or nested under it (separator-aware, so
21
- * `/a/mirror-2` is NOT treated as living inside `/a/mirror`). */
22
- const isUnder = (base, dir) => base === dir || base.startsWith(dir + path.sep) || base.startsWith(dir + '/')
23
-
24
- /**
25
- * Resolve which remote workspace a LOCAL path belongs to by scanning the mirror
26
- * registry for the mirror that contains it.
27
- *
28
- * Mirrors live exactly one level under the registry root as
29
- * `<host>-<user>-<port>/<basename>`, and `mirrorDirFor()` appends a short hash
30
- * when two remote paths share a basename, so containment is unambiguous. The
31
- * longest containing path still wins, which keeps the result stable if a
32
- * future layout ever nests them.
33
- *
34
- * A mirror whose meta is missing, unparsable, or has no `host` is skipped
35
- * rather than guessed at: reporting no binding makes the caller refuse loudly,
36
- * while guessing would send commands to an unintended host.
37
- *
38
- * @param {string} local - absolute local path, typically a session's cwd.
39
- * @param {string} root - the mirror registry root (`$DSH_HOME/remote-workspaces`).
40
- * @returns {{mirrorDir: string|null, remotePath: string, machine: {host: string, port: number, username: string}|null}}
41
- * `machine` is the mirror-recorded origin, or null when `local` is not inside
42
- * a usable mirror (in which case `remotePath` is empty too).
43
- */
44
- export function resolveMirror(local, root) {
45
- let mirrorDir = null
46
- let remotePath = ''
47
- let machine = null
48
- if (!local || !root || !existsSync(root)) return { mirrorDir, remotePath, machine }
49
- const base = norm(local)
50
- let hostDirs
51
- try {
52
- hostDirs = readdirSync(root)
53
- } catch {
54
- return { mirrorDir, remotePath, machine }
55
- }
56
- for (const hostDir of hostDirs) {
57
- const hostPath = path.join(root, hostDir)
58
- if (!statSync(hostPath, { throwIfNoEntry: false })?.isDirectory?.()) continue
59
- let entries
60
- try {
61
- entries = readdirSync(hostPath)
62
- } catch {
63
- continue
64
- }
65
- for (const entry of entries) {
66
- const metaPath = path.join(hostPath, entry, '.dsh-remote-meta.json')
67
- if (!existsSync(metaPath)) continue
68
- const mirrorAbs = norm(path.join(hostPath, entry))
69
- if (!isUnder(base, mirrorAbs)) continue
70
- // Deepest match wins; an equal-length path cannot occur twice.
71
- if (mirrorDir && mirrorAbs.length <= mirrorDir.length) continue
72
- try {
73
- const meta = JSON.parse(readFileSync(metaPath, 'utf8'))
74
- if (!meta || !meta.host) continue
75
- mirrorDir = mirrorAbs
76
- remotePath = String(meta.remotePath || '')
77
- machine = {
78
- host: String(meta.host),
79
- port: Number(meta.port) || 22,
80
- username: String(meta.username || ''),
81
- }
82
- } catch { /* unparsable meta → not a usable binding */ }
83
- }
84
- }
85
- return { mirrorDir, remotePath, machine }
86
- }
87
-
88
- /**
89
- * Stable pool key for a remote identity. Two sessions on the same machine
90
- * produce the same key and therefore share one SSH connection; sessions on
91
- * different machines never collide.
92
- * @param {{host: string, port?: number|string, username?: string}} m - the remote identity.
93
- * @returns {string} the canonical `user@host:port` key.
94
- */
95
- export function poolKey(m) {
96
- return `${(m && m.username) || ''}@${(m && m.host) || ''}:${Number(m && m.port) || 22}`
97
- }
1
+ // Session → remote-binding resolution.
2
+ //
3
+ // A session's remote target is derived from its own workspace mirror, never
4
+ // from the plugin's mutable active-machine state. That is what keeps concurrent
5
+ // sessions on different hosts independent: with a single shared connection and
6
+ // a single shared workspace value, one session switching machines redirected
7
+ // every other session's commands to the wrong host (issue #25).
8
+ //
9
+ // The mirror directory is the durable record of that binding: `ensureMirror()`
10
+ // writes `.dsh-remote-meta.json` with the remote origin (`host`/`port`/
11
+ // `username`/`remotePath`) when the workspace is picked, so a session's cwd
12
+ // alone identifies both the machine to connect to and the workspace root.
13
+
14
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
15
+ import path from 'node:path'
16
+
17
+ /** Strip trailing separators so a mirror root and a path inside it compare cleanly. */
18
+ const norm = (p) => path.resolve(String(p || '')).replace(/[\\/]+$/, '') || ''
19
+
20
+ /** Whether `base` is `dir` itself or nested under it (separator-aware, so
21
+ * `/a/mirror-2` is NOT treated as living inside `/a/mirror`). */
22
+ const isUnder = (base, dir) => base === dir || base.startsWith(dir + path.sep) || base.startsWith(dir + '/')
23
+
24
+ /**
25
+ * Resolve which remote workspace a LOCAL path belongs to by scanning the mirror
26
+ * registry for the mirror that contains it.
27
+ *
28
+ * Mirrors live exactly one level under the registry root as
29
+ * `<host>-<user>-<port>/<basename>`, and `mirrorDirFor()` appends a short hash
30
+ * when two remote paths share a basename, so containment is unambiguous. The
31
+ * longest containing path still wins, which keeps the result stable if a
32
+ * future layout ever nests them.
33
+ *
34
+ * A mirror whose meta is missing, unparsable, or has no `host` is skipped
35
+ * rather than guessed at: reporting no binding makes the caller refuse loudly,
36
+ * while guessing would send commands to an unintended host.
37
+ *
38
+ * @param {string} local - absolute local path, typically a session's cwd.
39
+ * @param {string} root - the mirror registry root (`$DSH_HOME/remote-workspaces`).
40
+ * @returns {{mirrorDir: string|null, remotePath: string, machine: {host: string, port: number, username: string}|null}}
41
+ * `machine` is the mirror-recorded origin, or null when `local` is not inside
42
+ * a usable mirror (in which case `remotePath` is empty too).
43
+ */
44
+ export function resolveMirror(local, root) {
45
+ let mirrorDir = null
46
+ let remotePath = ''
47
+ let machine = null
48
+ if (!local || !root || !existsSync(root)) return { mirrorDir, remotePath, machine }
49
+ const base = norm(local)
50
+ let hostDirs
51
+ try {
52
+ hostDirs = readdirSync(root)
53
+ } catch {
54
+ return { mirrorDir, remotePath, machine }
55
+ }
56
+ for (const hostDir of hostDirs) {
57
+ const hostPath = path.join(root, hostDir)
58
+ if (!statSync(hostPath, { throwIfNoEntry: false })?.isDirectory?.()) continue
59
+ let entries
60
+ try {
61
+ entries = readdirSync(hostPath)
62
+ } catch {
63
+ continue
64
+ }
65
+ for (const entry of entries) {
66
+ const metaPath = path.join(hostPath, entry, '.dsh-remote-meta.json')
67
+ if (!existsSync(metaPath)) continue
68
+ const mirrorAbs = norm(path.join(hostPath, entry))
69
+ if (!isUnder(base, mirrorAbs)) continue
70
+ // Deepest match wins; an equal-length path cannot occur twice.
71
+ if (mirrorDir && mirrorAbs.length <= mirrorDir.length) continue
72
+ try {
73
+ const meta = JSON.parse(readFileSync(metaPath, 'utf8'))
74
+ if (!meta || !meta.host) continue
75
+ mirrorDir = mirrorAbs
76
+ remotePath = String(meta.remotePath || '')
77
+ machine = {
78
+ host: String(meta.host),
79
+ port: Number(meta.port) || 22,
80
+ username: String(meta.username || ''),
81
+ }
82
+ } catch { /* unparsable meta → not a usable binding */ }
83
+ }
84
+ }
85
+ return { mirrorDir, remotePath, machine }
86
+ }
87
+
88
+ /**
89
+ * Stable pool key for a remote identity. Two sessions on the same machine
90
+ * produce the same key and therefore share one SSH connection; sessions on
91
+ * different machines never collide.
92
+ * @param {{host: string, port?: number|string, username?: string}} m - the remote identity.
93
+ * @returns {string} the canonical `user@host:port` key.
94
+ */
95
+ export function poolKey(m) {
96
+ return `${(m && m.username) || ''}@${(m && m.host) || ''}:${Number(m && m.port) || 22}`
97
+ }