dsh-remote 0.8.10 → 0.8.12
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/cordis.patch.yml +53 -53
- package/lib/binding.js +97 -0
- package/lib/client.js +1986 -1496
- package/lib/index.js +395 -127
- package/package.json +1 -1
package/cordis.patch.yml
CHANGED
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
# dsh-remote bundle patch: inserts this plugin into a profile's layer stack.
|
|
2
|
-
# Applied when a profile lists this bundle; rows reference the package by name so
|
|
3
|
-
# Node resolution finds the installed code.
|
|
4
|
-
#
|
|
5
|
-
# Since v0.7.2 dsh-remote ships dsh-better-sidebar as a hard dependency and
|
|
6
|
-
# mounts it here, so `dsh plugin add dsh-remote` brings up the sidebar with
|
|
7
|
-
# zero extra steps. The sidebar row uses a DEDICATED id (`dsh-remote-sidebar`)
|
|
8
|
-
# and a guard so it backs off when another row mounts `dsh-better-sidebar`
|
|
9
|
-
# (e.g. the user installed the sidebar bundle on its own, or an aggregate
|
|
10
|
-
# bundle owns it) — two enabled mounts would both register /sidebar/api and
|
|
11
|
-
# fail the whole plugin tree at boot.
|
|
12
|
-
#
|
|
13
|
-
# Order-independence (issue #12): the guard must NOT depend on
|
|
14
|
-
# `ctx.loader.entries()` creation order. The loader creates entries in bundle
|
|
15
|
-
# order and each row's disabled expression is evaluated at creation time, when
|
|
16
|
-
# only rows created BEFORE it are visible — so with
|
|
17
|
-
# `bundles: [..., "dsh-remote", "dsh-better-sidebar", ...]` (standalone sidebar
|
|
18
|
-
# installed after dsh-remote, the normal `dsh plugin add` append order) both
|
|
19
|
-
# rows saw no other sidebar mount and BOTH started, crashing boot with
|
|
20
|
-
# `duplicate prefix route "/sidebar/api"`.
|
|
21
|
-
#
|
|
22
|
-
# Instead we scan the COMPOSED PATCH STACK — the include entry's
|
|
23
|
-
# `config.patches` (every bundle layer in `dsh.profile.bundles` order, then the
|
|
24
|
-
# profile/home/overlay layers) — for any OTHER row that mounts
|
|
25
|
-
# `dsh-better-sidebar`. That data is fully materialized before any row guard
|
|
26
|
-
# evaluates, so the outcome is identical no matter where the standalone bundle
|
|
27
|
-
# sits in the bundle list. It is also pure data: it never reads another row's
|
|
28
|
-
# `disabled` (reading another row's disabled expression would recursively
|
|
29
|
-
# re-enter the loader — dsh-better-sidebar's own row carries one too, so the
|
|
30
|
-
# two would ping-pong), so no recursion is possible. The old entries()-based
|
|
31
|
-
# check is kept as a belt-and-suspenders fallback for rows not visible in the
|
|
32
|
-
# patch stack (e.g. an entry injected at runtime by HMR or another plugin).
|
|
33
|
-
- insert:
|
|
34
|
-
- id: dsh-remote
|
|
35
|
-
name: 'dsh-remote'
|
|
36
|
-
- id: dsh-remote-sidebar
|
|
37
|
-
name: 'dsh-better-sidebar'
|
|
38
|
-
disabled: !!js >-
|
|
39
|
-
(function () {
|
|
40
|
-
try {
|
|
41
|
-
const inc = ctx.loader && ctx.loader.resolve && ctx.loader.resolve('include')
|
|
42
|
-
const patches = inc && inc.subtree && inc.subtree.config && inc.subtree.config.patches
|
|
43
|
-
if (Array.isArray(patches)) {
|
|
44
|
-
for (const p of patches) {
|
|
45
|
-
if (!p || !Array.isArray(p.insert)) continue
|
|
46
|
-
for (const row of p.insert) {
|
|
47
|
-
if (row && row.name === 'dsh-better-sidebar' && row.id !== 'dsh-remote-sidebar') return true
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
} catch (e) {}
|
|
52
|
-
return [...ctx.loader.entries()].some((e) => e.options.name === 'dsh-better-sidebar' && e.options.id !== 'dsh-remote-sidebar')
|
|
53
|
-
})()
|
|
1
|
+
# dsh-remote bundle patch: inserts this plugin into a profile's layer stack.
|
|
2
|
+
# Applied when a profile lists this bundle; rows reference the package by name so
|
|
3
|
+
# Node resolution finds the installed code.
|
|
4
|
+
#
|
|
5
|
+
# Since v0.7.2 dsh-remote ships dsh-better-sidebar as a hard dependency and
|
|
6
|
+
# mounts it here, so `dsh plugin add dsh-remote` brings up the sidebar with
|
|
7
|
+
# zero extra steps. The sidebar row uses a DEDICATED id (`dsh-remote-sidebar`)
|
|
8
|
+
# and a guard so it backs off when another row mounts `dsh-better-sidebar`
|
|
9
|
+
# (e.g. the user installed the sidebar bundle on its own, or an aggregate
|
|
10
|
+
# bundle owns it) — two enabled mounts would both register /sidebar/api and
|
|
11
|
+
# fail the whole plugin tree at boot.
|
|
12
|
+
#
|
|
13
|
+
# Order-independence (issue #12): the guard must NOT depend on
|
|
14
|
+
# `ctx.loader.entries()` creation order. The loader creates entries in bundle
|
|
15
|
+
# order and each row's disabled expression is evaluated at creation time, when
|
|
16
|
+
# only rows created BEFORE it are visible — so with
|
|
17
|
+
# `bundles: [..., "dsh-remote", "dsh-better-sidebar", ...]` (standalone sidebar
|
|
18
|
+
# installed after dsh-remote, the normal `dsh plugin add` append order) both
|
|
19
|
+
# rows saw no other sidebar mount and BOTH started, crashing boot with
|
|
20
|
+
# `duplicate prefix route "/sidebar/api"`.
|
|
21
|
+
#
|
|
22
|
+
# Instead we scan the COMPOSED PATCH STACK — the include entry's
|
|
23
|
+
# `config.patches` (every bundle layer in `dsh.profile.bundles` order, then the
|
|
24
|
+
# profile/home/overlay layers) — for any OTHER row that mounts
|
|
25
|
+
# `dsh-better-sidebar`. That data is fully materialized before any row guard
|
|
26
|
+
# evaluates, so the outcome is identical no matter where the standalone bundle
|
|
27
|
+
# sits in the bundle list. It is also pure data: it never reads another row's
|
|
28
|
+
# `disabled` (reading another row's disabled expression would recursively
|
|
29
|
+
# re-enter the loader — dsh-better-sidebar's own row carries one too, so the
|
|
30
|
+
# two would ping-pong), so no recursion is possible. The old entries()-based
|
|
31
|
+
# check is kept as a belt-and-suspenders fallback for rows not visible in the
|
|
32
|
+
# patch stack (e.g. an entry injected at runtime by HMR or another plugin).
|
|
33
|
+
- insert:
|
|
34
|
+
- id: dsh-remote
|
|
35
|
+
name: 'dsh-remote'
|
|
36
|
+
- id: dsh-remote-sidebar
|
|
37
|
+
name: 'dsh-better-sidebar'
|
|
38
|
+
disabled: !!js >-
|
|
39
|
+
(function () {
|
|
40
|
+
try {
|
|
41
|
+
const inc = ctx.loader && ctx.loader.resolve && ctx.loader.resolve('include')
|
|
42
|
+
const patches = inc && inc.subtree && inc.subtree.config && inc.subtree.config.patches
|
|
43
|
+
if (Array.isArray(patches)) {
|
|
44
|
+
for (const p of patches) {
|
|
45
|
+
if (!p || !Array.isArray(p.insert)) continue
|
|
46
|
+
for (const row of p.insert) {
|
|
47
|
+
if (row && row.name === 'dsh-better-sidebar' && row.id !== 'dsh-remote-sidebar') return true
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} catch (e) {}
|
|
52
|
+
return [...ctx.loader.entries()].some((e) => e.options.name === 'dsh-better-sidebar' && e.options.id !== 'dsh-remote-sidebar')
|
|
53
|
+
})()
|
package/lib/binding.js
ADDED
|
@@ -0,0 +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
|
+
}
|