dsh-remote 0.8.47 → 0.8.49

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 (132) hide show
  1. package/lib/client.js +89 -16
  2. package/lib/ignore.js +46 -0
  3. package/lib/index.js +68 -23
  4. package/lib/remote-shell-child.cjs +305 -0
  5. package/lib/remote-shell.js +518 -0
  6. package/lib/search.js +8 -3
  7. package/package.json +1 -1
  8. package/node_modules/buildcheck/.eslintrc.js +0 -5
  9. package/node_modules/buildcheck/.github/workflows/ci.yml +0 -88
  10. package/node_modules/buildcheck/.github/workflows/lint.yml +0 -27
  11. package/node_modules/buildcheck/.npmignore +0 -3
  12. package/node_modules/buildcheck/LICENSE +0 -19
  13. package/node_modules/buildcheck/README.md +0 -106
  14. package/node_modules/buildcheck/deps/Find-VisualStudio.cs +0 -250
  15. package/node_modules/buildcheck/lib/findvs.js +0 -361
  16. package/node_modules/buildcheck/lib/index.js +0 -738
  17. package/node_modules/buildcheck/package.json +0 -36
  18. package/node_modules/buildcheck/test/test.js +0 -8
  19. package/node_modules/cpu-features/.eslintrc.js +0 -5
  20. package/node_modules/cpu-features/.github/workflows/ci.yml +0 -74
  21. package/node_modules/cpu-features/.github/workflows/lint.yml +0 -27
  22. package/node_modules/cpu-features/LICENSE +0 -19
  23. package/node_modules/cpu-features/README.md +0 -59
  24. package/node_modules/cpu-features/binding.gyp +0 -16
  25. package/node_modules/cpu-features/buildcheck.js +0 -32
  26. package/node_modules/cpu-features/deps/cpu_features/.clang-format +0 -4
  27. package/node_modules/cpu-features/deps/cpu_features/.dockerignore +0 -31
  28. package/node_modules/cpu-features/deps/cpu_features/.github/workflows/Dockerfile +0 -5
  29. package/node_modules/cpu-features/deps/cpu_features/.github/workflows/aarch64_linux_cmake.yml +0 -30
  30. package/node_modules/cpu-features/deps/cpu_features/.github/workflows/amd64_freebsd_cmake.yml +0 -22
  31. package/node_modules/cpu-features/deps/cpu_features/.github/workflows/amd64_linux_bazel.yml +0 -26
  32. package/node_modules/cpu-features/deps/cpu_features/.github/workflows/amd64_linux_cmake.yml +0 -31
  33. package/node_modules/cpu-features/deps/cpu_features/.github/workflows/amd64_macos_cmake.yml +0 -43
  34. package/node_modules/cpu-features/deps/cpu_features/.github/workflows/amd64_windows_cmake.yml +0 -25
  35. package/node_modules/cpu-features/deps/cpu_features/.github/workflows/arm_linux_cmake.yml +0 -31
  36. package/node_modules/cpu-features/deps/cpu_features/.github/workflows/clang_format.yml +0 -24
  37. package/node_modules/cpu-features/deps/cpu_features/.github/workflows/mips_linux_cmake.yml +0 -30
  38. package/node_modules/cpu-features/deps/cpu_features/.github/workflows/power_linux_cmake.yml +0 -29
  39. package/node_modules/cpu-features/deps/cpu_features/.github/workflows/riscv_linux_cmake.yml +0 -28
  40. package/node_modules/cpu-features/deps/cpu_features/.github/workflows/s390x_linux_cmake.yml +0 -27
  41. package/node_modules/cpu-features/deps/cpu_features/.grenrc.yml +0 -21
  42. package/node_modules/cpu-features/deps/cpu_features/BUILD.bazel +0 -329
  43. package/node_modules/cpu-features/deps/cpu_features/CMakeLists.txt +0 -261
  44. package/node_modules/cpu-features/deps/cpu_features/CONTRIBUTING.md +0 -23
  45. package/node_modules/cpu-features/deps/cpu_features/LICENSE +0 -230
  46. package/node_modules/cpu-features/deps/cpu_features/README.md +0 -272
  47. package/node_modules/cpu-features/deps/cpu_features/WORKSPACE +0 -19
  48. package/node_modules/cpu-features/deps/cpu_features/bazel/ci/README.md +0 -5
  49. package/node_modules/cpu-features/deps/cpu_features/bazel/platforms.bzl +0 -11
  50. package/node_modules/cpu-features/deps/cpu_features/cmake/CpuFeaturesConfig.cmake.in +0 -3
  51. package/node_modules/cpu-features/deps/cpu_features/cmake/CpuFeaturesNdkCompatConfig.cmake.in +0 -3
  52. package/node_modules/cpu-features/deps/cpu_features/cmake/README.md +0 -30
  53. package/node_modules/cpu-features/deps/cpu_features/cmake/ci/Makefile +0 -252
  54. package/node_modules/cpu-features/deps/cpu_features/cmake/ci/README.md +0 -40
  55. package/node_modules/cpu-features/deps/cpu_features/cmake/ci/doc/docker.dot +0 -64
  56. package/node_modules/cpu-features/deps/cpu_features/cmake/ci/doc/docker.svg +0 -312
  57. package/node_modules/cpu-features/deps/cpu_features/cmake/ci/doc/generate_image.sh +0 -7
  58. package/node_modules/cpu-features/deps/cpu_features/cmake/ci/docker/amd64/Dockerfile +0 -48
  59. package/node_modules/cpu-features/deps/cpu_features/cmake/ci/docker/toolchain/Dockerfile +0 -34
  60. package/node_modules/cpu-features/deps/cpu_features/cmake/ci/sample/CMakeLists.txt +0 -22
  61. package/node_modules/cpu-features/deps/cpu_features/cmake/ci/sample/main.cpp +0 -11
  62. package/node_modules/cpu-features/deps/cpu_features/cmake/ci/vagrant/freebsd/Vagrantfile +0 -107
  63. package/node_modules/cpu-features/deps/cpu_features/cmake/googletest.CMakeLists.txt.in +0 -15
  64. package/node_modules/cpu-features/deps/cpu_features/cpu_features.gyp +0 -105
  65. package/node_modules/cpu-features/deps/cpu_features/include/cpu_features_cache_info.h +0 -54
  66. package/node_modules/cpu-features/deps/cpu_features/include/cpu_features_macros.h +0 -384
  67. package/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_aarch64.h +0 -259
  68. package/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_arm.h +0 -121
  69. package/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_mips.h +0 -74
  70. package/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_ppc.h +0 -149
  71. package/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_riscv.h +0 -72
  72. package/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_s390x.h +0 -108
  73. package/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_x86.h +0 -288
  74. package/node_modules/cpu-features/deps/cpu_features/include/internal/bit_utils.h +0 -40
  75. package/node_modules/cpu-features/deps/cpu_features/include/internal/cpuid_x86.h +0 -37
  76. package/node_modules/cpu-features/deps/cpu_features/include/internal/filesystem.h +0 -39
  77. package/node_modules/cpu-features/deps/cpu_features/include/internal/hwcaps.h +0 -240
  78. package/node_modules/cpu-features/deps/cpu_features/include/internal/stack_line_reader.h +0 -49
  79. package/node_modules/cpu-features/deps/cpu_features/include/internal/string_view.h +0 -110
  80. package/node_modules/cpu-features/deps/cpu_features/include/internal/windows_utils.h +0 -70
  81. package/node_modules/cpu-features/deps/cpu_features/ndk_compat/CMakeLists.txt +0 -60
  82. package/node_modules/cpu-features/deps/cpu_features/ndk_compat/README.md +0 -4
  83. package/node_modules/cpu-features/deps/cpu_features/ndk_compat/cpu-features.c +0 -205
  84. package/node_modules/cpu-features/deps/cpu_features/ndk_compat/cpu-features.h +0 -320
  85. package/node_modules/cpu-features/deps/cpu_features/ndk_compat/ndk-compat-test.c +0 -12
  86. package/node_modules/cpu-features/deps/cpu_features/patches/0001-Add-Apple-Silicon-Support.patch +0 -456
  87. package/node_modules/cpu-features/deps/cpu_features/scripts/generate_badges.d +0 -165
  88. package/node_modules/cpu-features/deps/cpu_features/scripts/make_release.sh +0 -75
  89. package/node_modules/cpu-features/deps/cpu_features/scripts/run_integration.sh +0 -490
  90. package/node_modules/cpu-features/deps/cpu_features/scripts/test_integration.sh +0 -79
  91. package/node_modules/cpu-features/deps/cpu_features/src/copy.inl +0 -19
  92. package/node_modules/cpu-features/deps/cpu_features/src/define_introspection.inl +0 -86
  93. package/node_modules/cpu-features/deps/cpu_features/src/define_introspection_and_hwcaps.inl +0 -26
  94. package/node_modules/cpu-features/deps/cpu_features/src/equals.inl +0 -22
  95. package/node_modules/cpu-features/deps/cpu_features/src/filesystem.c +0 -62
  96. package/node_modules/cpu-features/deps/cpu_features/src/hwcaps.c +0 -169
  97. package/node_modules/cpu-features/deps/cpu_features/src/impl_aarch64__base_implementation.inl +0 -88
  98. package/node_modules/cpu-features/deps/cpu_features/src/impl_aarch64_linux_or_android.c +0 -79
  99. package/node_modules/cpu-features/deps/cpu_features/src/impl_aarch64_macos_or_iphone.c +0 -82
  100. package/node_modules/cpu-features/deps/cpu_features/src/impl_aarch64_windows.c +0 -138
  101. package/node_modules/cpu-features/deps/cpu_features/src/impl_arm_linux_or_android.c +0 -212
  102. package/node_modules/cpu-features/deps/cpu_features/src/impl_mips_linux_or_android.c +0 -93
  103. package/node_modules/cpu-features/deps/cpu_features/src/impl_ppc_linux.c +0 -163
  104. package/node_modules/cpu-features/deps/cpu_features/src/impl_riscv_linux.c +0 -111
  105. package/node_modules/cpu-features/deps/cpu_features/src/impl_s390x_linux.c +0 -120
  106. package/node_modules/cpu-features/deps/cpu_features/src/impl_x86__base_implementation.inl +0 -2055
  107. package/node_modules/cpu-features/deps/cpu_features/src/impl_x86_freebsd.c +0 -68
  108. package/node_modules/cpu-features/deps/cpu_features/src/impl_x86_linux_or_android.c +0 -58
  109. package/node_modules/cpu-features/deps/cpu_features/src/impl_x86_macos.c +0 -57
  110. package/node_modules/cpu-features/deps/cpu_features/src/impl_x86_windows.c +0 -58
  111. package/node_modules/cpu-features/deps/cpu_features/src/stack_line_reader.c +0 -132
  112. package/node_modules/cpu-features/deps/cpu_features/src/string_view.c +0 -192
  113. package/node_modules/cpu-features/deps/cpu_features/src/utils/list_cpu_features.c +0 -461
  114. package/node_modules/cpu-features/deps/cpu_features/test/CMakeLists.txt +0 -114
  115. package/node_modules/cpu-features/deps/cpu_features/test/bit_utils_test.cc +0 -53
  116. package/node_modules/cpu-features/deps/cpu_features/test/cpuinfo_aarch64_test.cc +0 -396
  117. package/node_modules/cpu-features/deps/cpu_features/test/cpuinfo_arm_test.cc +0 -384
  118. package/node_modules/cpu-features/deps/cpu_features/test/cpuinfo_mips_test.cc +0 -176
  119. package/node_modules/cpu-features/deps/cpu_features/test/cpuinfo_ppc_test.cc +0 -131
  120. package/node_modules/cpu-features/deps/cpu_features/test/cpuinfo_riscv_test.cc +0 -180
  121. package/node_modules/cpu-features/deps/cpu_features/test/cpuinfo_s390x_test.cc +0 -82
  122. package/node_modules/cpu-features/deps/cpu_features/test/cpuinfo_x86_test.cc +0 -1729
  123. package/node_modules/cpu-features/deps/cpu_features/test/filesystem_for_testing.cc +0 -103
  124. package/node_modules/cpu-features/deps/cpu_features/test/filesystem_for_testing.h +0 -61
  125. package/node_modules/cpu-features/deps/cpu_features/test/hwcaps_for_testing.cc +0 -52
  126. package/node_modules/cpu-features/deps/cpu_features/test/hwcaps_for_testing.h +0 -31
  127. package/node_modules/cpu-features/deps/cpu_features/test/stack_line_reader_test.cc +0 -132
  128. package/node_modules/cpu-features/deps/cpu_features/test/string_view_test.cc +0 -202
  129. package/node_modules/cpu-features/lib/index.js +0 -5
  130. package/node_modules/cpu-features/package.json +0 -40
  131. package/node_modules/cpu-features/src/binding.cc +0 -151
  132. package/node_modules/cpu-features/test/test.js +0 -12
@@ -0,0 +1,518 @@
1
+ /**
2
+ * Remote terminal as a shell choice — the host terminal's own option list.
3
+ *
4
+ * The built-in terminal is a *local* pty whose program comes from
5
+ * `dsh-api-terminal-controller`'s verified shell list (`shellCandidates`). A
6
+ * plugin cannot hand that pty a command line, but it CAN contribute one entry
7
+ * to that list: an absolute path to a launcher we generate. The pty then runs
8
+ * our launcher as a local process, and the launcher execs `ssh` with inherited
9
+ * stdio — which is exactly how the terminal should behave. Resizing, key
10
+ * binding, scrollback and color all stay the host's job; nothing is re-implemented
11
+ * here.
12
+ *
13
+ * The launcher itself is dumb (find node, run the child script). The child
14
+ * (`remote-shell-child.cjs`) resolves the remote target from the checkout it is
15
+ * started in and replaces itself with `ssh`.
16
+ *
17
+ * @module dsh-remote/remote-shell
18
+ */
19
+
20
+ import { chmodSync, existsSync, mkdirSync, readdirSync, unlinkSync, writeFileSync } from 'node:fs'
21
+ import path from 'node:path'
22
+
23
+ /** Label shown in the terminal's shell menu.
24
+ *
25
+ * Windows needs a real `.cmd` on disk (ConPTY cannot start an extensionless
26
+ * file), but the label does not have to advertise it: the menu shows the
27
+ * candidate's basename, and `shellCandidate` contributes the bare name so the
28
+ * host resolves it through `PATH` + `PATHEXT`. The suffix is therefore an
29
+ * implementation detail of the file on disk, not of the menu entry. */
30
+ export const SHELL_OPTION_NAME = 'remote ssh'
31
+
32
+ /** Absolute path of the generated launcher for this platform. */
33
+ export function shellLauncherPath(dir, platform = process.platform) {
34
+ return path.join(dir, platform === 'win32' ? SHELL_OPTION_NAME + '.cmd' : SHELL_OPTION_NAME)
35
+ }
36
+
37
+ /** Default `PATHEXT`, used when the environment does not carry one. */
38
+ const DEFAULT_PATH_EXT = '.COM;.EXE;.BAT;.CMD'
39
+
40
+ /**
41
+ * The value to contribute to `shellCandidates`.
42
+ *
43
+ * The menu label is `executableName(candidate)` — the candidate's basename, with
44
+ * no extension stripped — while the path that actually runs is whatever the host
45
+ * resolves. Those are two different things, and the gap is the only way to get a
46
+ * clean label on Windows.
47
+ *
48
+ * A file path would be shown as `remote ssh.cmd`, because the terminal has to
49
+ * start a real `.cmd` and ConPTY refuses anything else (a file with no extension
50
+ * dies with 193, `ERROR_BAD_EXE_FORMAT`). So on Windows the *bare* name is
51
+ * contributed instead: the host resolves it through `PATH` + `PATHEXT`, lands on
52
+ * `remote ssh.cmd`, and the label stays `remote ssh`. That is exactly the rule
53
+ * `resolveExecutable` documents — an absolute path is checked as-is with no
54
+ * extension guessing, a bare name is looked up per `PATHEXT`.
55
+ *
56
+ * Two consequences, both handled by the caller: the launcher's directory has to
57
+ * be on `PATH` (see `appendPathEntry`), and the name must contain no dot, since
58
+ * a bare name is only extended when `extname()` is empty.
59
+ *
60
+ * POSIX has no such rule — the file is directly executable — so the absolute
61
+ * path is contributed as-is.
62
+ *
63
+ * @param {string} platform
64
+ * @param {string} launcherPath
65
+ * @returns {string} candidate value for the shell list
66
+ */
67
+ export function shellCandidate(platform, launcherPath) {
68
+ if (platform !== 'win32') return launcherPath
69
+ return path.basename(launcherPath).replace(/\.cmd$/i, '')
70
+ }
71
+
72
+ /**
73
+ * Resolve a bare command name the way the host does, without spawning anything.
74
+ *
75
+ * A mirror of `subprocess-local`'s `executableCandidates` + `resolveExecutable`:
76
+ * directory-major order over `PATH`, extension-minor over `PATHEXT`, and
77
+ * extensions only when the name has none of its own. Used to decide whether the
78
+ * bare-name contribution will actually resolve; if it will not, the caller falls
79
+ * back to the absolute path and accepts the `.cmd` in the label.
80
+ *
81
+ * @param {string} name
82
+ * @param {{ platform?: string, pathEnv?: string, pathExt?: string, cwd?: string,
83
+ * fs?: { existsSync: (p: string) => boolean } }} [deps]
84
+ * @returns {string} resolved path, or '' when nothing matches
85
+ */
86
+ export function resolveThroughPath(name, deps = {}) {
87
+ const platform = deps.platform || process.platform
88
+ const fs = deps.fs || { existsSync }
89
+ const pathEnv = deps.pathEnv === undefined ? (process.env.PATH || '') : deps.pathEnv
90
+ const pathExt = deps.pathExt === undefined
91
+ ? (process.env.PATHEXT || DEFAULT_PATH_EXT)
92
+ : deps.pathExt
93
+ const cwd = deps.cwd || process.cwd()
94
+ // The platform being MODELED owns the path semantics, not the platform this
95
+ // process runs on: a win32 lookup splits PATH on ';' and understands drive
96
+ // letters, which the runtime `path` only does when the host is Windows. On a
97
+ // real host the two coincide, so this changes nothing outside tests.
98
+ const pathApi = platform === 'win32' ? path.win32 : path.posix
99
+ const extensions = platform === 'win32' && pathApi.extname(name) === ''
100
+ ? pathExt.split(';').filter(Boolean)
101
+ : ['']
102
+ const delimiter = platform === 'win32' ? ';' : ':'
103
+ for (const directory of pathEnv.split(delimiter)) {
104
+ if (!directory) continue
105
+ for (const extension of extensions) {
106
+ const candidate = pathApi.resolve(cwd, directory, name + extension)
107
+ try {
108
+ if (fs.existsSync(candidate)) return candidate
109
+ } catch { /* an unreadable directory is simply skipped */ }
110
+ }
111
+ }
112
+ return ''
113
+ }
114
+
115
+ /** Per-environment ownership of the entries this module added. */
116
+ const pathEntries = new WeakMap()
117
+
118
+ /**
119
+ * Append a directory to the process `PATH`, idempotently and ref-counted.
120
+ *
121
+ * Appended, never prepended: a plugin has no business shadowing a program the
122
+ * user already has, and the launcher's name cannot collide by accident.
123
+ *
124
+ * The entry is ref-counted per environment object because the contribution can
125
+ * be installed more than once in a process lifetime — a reload disposes the old
126
+ * effect and installs a new one, and the two callbacks may interleave. A plain
127
+ * "set/clear" pair would let the first disposal strip the entry out from under
128
+ * the still-live registration, which only shows up much later as the menu entry
129
+ * silently vanishing (the host cannot resolve a bare name that is no longer on
130
+ * PATH).
131
+ *
132
+ * A directory that was already on `PATH` before we touched it is marked as
133
+ * externally owned and is never removed by us.
134
+ *
135
+ * @param {string} dir - absolute directory to expose
136
+ * @param {object} [env] - environment object; defaults to `process.env`
137
+ * @returns {() => void} undo, safe to call more than once
138
+ * @throws {Error} when the variable cannot be written
139
+ */
140
+ export function appendPathEntry(dir, env = process.env) {
141
+ const absolute = path.resolve(dir)
142
+ const key = Object.keys(env).find((name) => name.toUpperCase() === 'PATH') || 'PATH'
143
+ let owned = pathEntries.get(env)
144
+ if (!owned) {
145
+ owned = new Map()
146
+ pathEntries.set(env, owned)
147
+ }
148
+ const count = owned.get(absolute)
149
+ if (count === undefined) {
150
+ const current = env[key] || ''
151
+ const parts = current.split(path.delimiter).filter(Boolean)
152
+ if (parts.some((entry) => path.resolve(entry) === absolute)) {
153
+ owned.set(absolute, 0) // pre-existing: ours to use, not ours to remove
154
+ } else {
155
+ env[key] = current === '' ? dir : current + path.delimiter + dir
156
+ owned.set(absolute, 1)
157
+ }
158
+ } else if (count > 0) {
159
+ owned.set(absolute, count + 1)
160
+ }
161
+ return () => {
162
+ const live = owned.get(absolute)
163
+ if (live === undefined || live <= 0) return
164
+ if (live > 1) {
165
+ owned.set(absolute, live - 1)
166
+ return
167
+ }
168
+ owned.delete(absolute)
169
+ const kept = (env[key] || '')
170
+ .split(path.delimiter)
171
+ .filter((entry) => entry !== '' && path.resolve(entry) !== absolute)
172
+ env[key] = kept.join(path.delimiter)
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Node interpreters worth baking into the launcher, most specific first.
178
+ *
179
+ * The desktop host does not put its bundled node on a child's PATH (by design),
180
+ * so an absolute path is the only reliable option; `where node` is a last
181
+ * resort. When no node is found the launcher falls back to re-entering the
182
+ * Electron binary with `ELECTRON_RUN_AS_NODE`, which needs no second runtime.
183
+ *
184
+ * Electron puts its resources beside the executable on Windows and Linux, but
185
+ * under `Contents/Resources` on macOS, so the path derived from `execPath` has
186
+ * to follow the platform; the `resourcesPath` candidate already does.
187
+ *
188
+ * @param {{ execPath?: string, resourcesPath?: string, platform?: string, env?: object }} deps
189
+ * @returns {string[]} existing interpreter candidates, in preference order
190
+ */
191
+ export function runtimeNodeCandidates(deps = {}) {
192
+ const platform = deps.platform || process.platform
193
+ const env = deps.env || process.env
194
+ // `undefined` means "ask the process", `''` means "this host has no such
195
+ // executable" — callers that model another platform rely on the difference.
196
+ const execPath = deps.execPath === undefined ? (process.execPath || '') : deps.execPath
197
+ const exe = platform === 'win32' ? 'node.exe' : 'node'
198
+ const bundledNode = ['runtime', 'primary-runtime', 'dependencies', 'node', 'bin', exe]
199
+ const out = []
200
+ const push = (value) => { if (value && !out.includes(value)) out.push(value) }
201
+ push(env.DSH_REMOTE_TERMINAL_NODE)
202
+ if (deps.resourcesPath) push(path.join(deps.resourcesPath, ...bundledNode))
203
+ if (execPath) {
204
+ const here = path.dirname(execPath)
205
+ push(path.join(platform === 'darwin' ? path.join(here, '..', 'Resources') : path.join(here, 'resources'), ...bundledNode))
206
+ // A plain node install: execPath IS the interpreter.
207
+ if (!deps.electron && path.basename(execPath).toLowerCase() === exe) push(execPath)
208
+ }
209
+ return out.filter((candidate) => {
210
+ try { return existsSync(candidate) } catch { return false }
211
+ })
212
+ }
213
+
214
+ /**
215
+ * Body of the generated launcher. Pure so it can be asserted without touching
216
+ * the filesystem.
217
+ *
218
+ * @param {{ platform?: string, nodePath?: string, electronPath?: string, childPath: string, machinesFile: string }} spec
219
+ * @returns {string} launcher source, runnable as-is
220
+ */
221
+ export function launcherSource(spec) {
222
+ const platform = spec.platform || process.platform
223
+ const child = spec.childPath
224
+ const machines = spec.machinesFile
225
+ if (platform === 'win32') {
226
+ const node = spec.nodePath || ''
227
+ const electron = spec.electronPath || ''
228
+ const lines = [
229
+ '@echo off',
230
+ 'rem Generated by dsh-remote - do not edit; rewritten on every load.',
231
+ 'setlocal',
232
+ `set "DSH_CHILD=${child}"`,
233
+ `set "DSH_MACHINES=${machines}"`,
234
+ `set "DSH_NODE=${node}"`,
235
+ 'if defined DSH_NODE if exist "%DSH_NODE%" goto :run',
236
+ 'set "DSH_NODE="',
237
+ 'for /f "delims=" %%N in (\'where node 2^>nul\') do if not defined DSH_NODE set "DSH_NODE=%%N"',
238
+ 'if defined DSH_NODE goto :run',
239
+ ]
240
+ // Re-entering the Electron binary as node only makes sense when this host
241
+ // IS an Electron app; a plain node install has nothing to re-enter.
242
+ if (electron) {
243
+ lines.push(
244
+ 'set "ELECTRON_RUN_AS_NODE=1"',
245
+ `"${electron}" "%DSH_CHILD%" --machines "%DSH_MACHINES%"`,
246
+ 'exit /b %ERRORLEVEL%',
247
+ )
248
+ }
249
+ lines.push(
250
+ 'echo [dsh-remote] no node interpreter found for the remote terminal. 1>&2',
251
+ 'echo [dsh-remote] set DSH_REMOTE_TERMINAL_NODE to an absolute node.exe path, or reinstall dsh-remote. 1>&2',
252
+ 'exit /b 127',
253
+ ':run',
254
+ '"%DSH_NODE%" "%DSH_CHILD%" --machines "%DSH_MACHINES%"',
255
+ 'exit /b %ERRORLEVEL%',
256
+ '',
257
+ )
258
+ return lines.join('\r\n')
259
+ }
260
+ const node = spec.nodePath || ''
261
+ return [
262
+ '#!/bin/sh',
263
+ '# Generated by dsh-remote - do not edit; rewritten on every load.',
264
+ `DSH_CHILD=${shqPosix(child)}`,
265
+ `DSH_MACHINES=${shqPosix(machines)}`,
266
+ node ? `DSH_NODE=${shqPosix(node)}` : 'DSH_NODE=',
267
+ 'if [ -n "$DSH_NODE" ] && [ -x "$DSH_NODE" ]; then exec "$DSH_NODE" "$DSH_CHILD" --machines "$DSH_MACHINES"; fi',
268
+ 'if command -v node >/dev/null 2>&1; then exec node "$DSH_CHILD" --machines "$DSH_MACHINES"; fi',
269
+ 'echo "[dsh-remote] no node interpreter found for the remote terminal." >&2',
270
+ 'exit 127',
271
+ '',
272
+ ].join('\n')
273
+ }
274
+
275
+ /** POSIX single-quote, for paths baked into a shell script. */
276
+ export function shqPosix(value) {
277
+ return "'" + String(value).replace(/'/g, "'\\''") + "'"
278
+ }
279
+
280
+ /**
281
+ * Remove a launcher left on disk under another spelling of the same name.
282
+ *
283
+ * The menu label IS the file name, so renaming the option renames the file. On
284
+ * a case-insensitive volume (NTFS, and APFS by default) writing the new spelling
285
+ * lands in the OLD directory entry and the menu keeps showing the previous
286
+ * name; on a case-sensitive one the old file would simply linger and, having
287
+ * been registered before, could still appear. Deleting the stale entry first
288
+ * makes a rename take effect on both.
289
+ *
290
+ * Best effort throughout: a launcher that cannot be cleaned up must not stop
291
+ * the new one from being written.
292
+ *
293
+ * @param {string} dir
294
+ * @param {string} keepName - basename that must survive
295
+ * @param {object} fs
296
+ */
297
+ function removeStaleLauncher(dir, keepName, fs) {
298
+ if (typeof fs.readdirSync !== 'function' || typeof fs.unlinkSync !== 'function') return
299
+ let entries
300
+ try {
301
+ entries = fs.readdirSync(dir)
302
+ } catch {
303
+ return // first run: the directory does not exist yet
304
+ }
305
+ for (const entry of entries) {
306
+ if (entry === keepName || entry.toLowerCase() !== keepName.toLowerCase()) continue
307
+ try { fs.unlinkSync(path.join(dir, entry)) } catch { /* keep going */ }
308
+ }
309
+ }
310
+
311
+ /**
312
+ * Write (or refresh) the launcher. Idempotent and cheap: the content is a pure
313
+ * function of this install's paths, so a plugin upgrade that moves the package
314
+ * rewrites the same file with the new child path.
315
+ *
316
+ * @param {{ dir: string, platform?: string, childPath: string, machinesFile: string, nodePath?: string, electronPath?: string, fs?: object }} spec
317
+ * @returns {{ path: string, source: string }}
318
+ */
319
+ export function writeLauncher(spec) {
320
+ const platform = spec.platform || process.platform
321
+ const fs = spec.fs || { mkdirSync, writeFileSync, chmodSync, readdirSync, unlinkSync }
322
+ fs.mkdirSync(spec.dir, { recursive: true })
323
+ const file = shellLauncherPath(spec.dir, platform)
324
+ // Before writing: on a case-insensitive filesystem the new name would
325
+ // otherwise be absorbed into the old entry.
326
+ removeStaleLauncher(spec.dir, path.basename(file), fs)
327
+ const source = launcherSource({ ...spec, platform })
328
+ fs.writeFileSync(file, source, 'utf8')
329
+ if (platform !== 'win32') {
330
+ try { fs.chmodSync(file, 0o755) } catch { /* a no-op on filesystems without modes */ }
331
+ }
332
+ return { path: file, source }
333
+ }
334
+
335
+ /**
336
+ * Add one entry to the host terminal's shell list.
337
+ *
338
+ * `shellCandidates` is the exact array `discoverShells` reads on every `shells()`
339
+ * call, so appending to it is live: the next time the user opens the shell menu
340
+ * the entry is there, and `spawn()` accepts the matching `shellPath` because it
341
+ * looks the path up in that same list. Removing it on dispose keeps the menu
342
+ * truthful when the plugin is unloaded.
343
+ *
344
+ * A host without the field (older line, or a controller rebuilt to hide the
345
+ * array) is not an error: the plugin simply contributes no shell. A host that
346
+ * HAS the field but refuses the write is a different story — that throws, so the
347
+ * caller can say why the option never appeared instead of leaving the user to
348
+ * guess.
349
+ *
350
+ * @param {object} controller - the `terminalController` service
351
+ * @param {string} candidate - absolute launcher path
352
+ * @returns {(() => void) | null} undo, or null when there is nothing to undo
353
+ * @throws {TypeError} when the host exposes a shell list that cannot be written
354
+ */
355
+ export function addShellCandidate(controller, candidate) {
356
+ const config = controller && controller.config
357
+ const list = config && config.shellCandidates
358
+ if (!Array.isArray(list)) return null
359
+ if (list.includes(candidate)) return null
360
+ list.push(candidate)
361
+ return () => {
362
+ const at = list.indexOf(candidate)
363
+ if (at >= 0) list.splice(at, 1)
364
+ }
365
+ }
366
+
367
+ /** Human-readable reason from anything thrown. */
368
+ function reason(err) {
369
+ return String((err && err.message) || err)
370
+ }
371
+
372
+ /** Read an environment variable case-insensitively, as Windows does. */
373
+ function envValue(env, name) {
374
+ if (!env) return undefined
375
+ if (env[name] !== undefined) return env[name]
376
+ const match = Object.keys(env).find((key) => key.toUpperCase() === name.toUpperCase())
377
+ return match === undefined ? undefined : env[match]
378
+ }
379
+
380
+ /**
381
+ * Write the launcher and put it in the terminal's shell menu.
382
+ *
383
+ * Kept out of the plugin entry so the whole contribution — write, publish,
384
+ * withdraw — can be exercised without booting the host. Every way this can
385
+ * fail to produce a visible menu entry is reported through `logger.warn`,
386
+ * because the failure mode that matters is silent: a launcher on disk plus no
387
+ * option in the menu is indistinguishable from "the feature was never built".
388
+ *
389
+ * @param {object} ctx - plugin context; only `effect`, `get('logger')` are used
390
+ * @param {object} controller - the resolved `terminalController` *service*
391
+ * @param {{ dir: string, childPath: string, machinesFile: string, platform?: string,
392
+ * nodePath?: string, electronPath?: string, electron?: boolean, logger?: object,
393
+ * env?: object, fs?: { existsSync: (p: string) => boolean } }} spec
394
+ * @returns {boolean} whether the option is (or already was) in the menu
395
+ */
396
+ export function installTerminalShell(ctx, controller, spec = {}) {
397
+ const logger = spec.logger !== undefined
398
+ ? spec.logger
399
+ : (ctx && typeof ctx.get === 'function' ? ctx.get('logger') : undefined)
400
+ const warn = (message) => {
401
+ try {
402
+ if (logger && typeof logger.warn === 'function') logger.warn(`dsh-remote: ${message}`)
403
+ } catch { /* a logger failure must not change the outcome */ }
404
+ }
405
+
406
+ const platform = spec.platform || process.platform
407
+ const electron = spec.electron === undefined
408
+ ? Boolean(process.versions && process.versions.electron)
409
+ : spec.electron
410
+ const nodePath = spec.nodePath === undefined
411
+ ? (runtimeNodeCandidates({
412
+ platform,
413
+ resourcesPath: process.resourcesPath,
414
+ execPath: process.execPath,
415
+ env: process.env,
416
+ electron,
417
+ })[0] || '')
418
+ : spec.nodePath
419
+ const electronPath = spec.electronPath === undefined
420
+ ? (electron ? process.execPath : '')
421
+ : spec.electronPath
422
+
423
+ let launcher
424
+ try {
425
+ launcher = writeLauncher({
426
+ dir: spec.dir,
427
+ platform,
428
+ childPath: spec.childPath,
429
+ machinesFile: spec.machinesFile,
430
+ nodePath,
431
+ electronPath,
432
+ })
433
+ } catch (err) {
434
+ // A read-only DSH_HOME must not take the plugin down: the terminal just
435
+ // keeps offering the shells it already had.
436
+ warn(`cannot write the remote terminal launcher: ${reason(err)}`)
437
+ return false
438
+ }
439
+
440
+ if (controller === undefined || controller === null) {
441
+ warn('the terminal controller is unavailable; the remote ssh option is unavailable')
442
+ return false
443
+ }
444
+ const list = controller.config && controller.config.shellCandidates
445
+ if (!Array.isArray(list)) {
446
+ // Either an older host, or the wrong object was passed in — the service is
447
+ // reached with `ctx.get('terminalController')`, not with the inject callback
448
+ // argument itself. Say which one it looks like.
449
+ const looksLikeContext = typeof controller.get === 'function' || typeof controller.inject === 'function'
450
+ warn(looksLikeContext
451
+ ? 'the terminal controller was not resolved (a plugin context was passed instead); the remote ssh option is unavailable'
452
+ : 'the terminal controller exposes no shellCandidates list; the remote ssh option is unavailable')
453
+ return false
454
+ }
455
+
456
+ // On Windows the label is the candidate's basename, so contributing a file
457
+ // path would show `remote ssh.cmd`. Contributing the bare name keeps the label
458
+ // clean, at the price of exposing the launcher directory on PATH — and only
459
+ // while the host can actually resolve that name.
460
+ const bare = shellCandidate(platform, launcher.path)
461
+ const needsPath = bare !== launcher.path
462
+ const env = spec.env || process.env
463
+ const fs = spec.fs || { existsSync }
464
+
465
+ let installed = false
466
+ const publish = () => {
467
+ // The PATH entry lives inside the effect, not outside it: if the effect ever
468
+ // re-runs, the new run must put the entry back that the previous disposal
469
+ // took away. `appendPathEntry` ref-counts, so interleaved install/dispose
470
+ // pairs cannot strand a live registration without PATH.
471
+ let restorePath
472
+ let candidate = launcher.path
473
+ if (needsPath) {
474
+ try {
475
+ restorePath = appendPathEntry(path.dirname(launcher.path), env)
476
+ const found = resolveThroughPath(bare, {
477
+ platform,
478
+ pathEnv: envValue(env, 'PATH') || '',
479
+ pathExt: envValue(env, 'PATHEXT'),
480
+ fs,
481
+ })
482
+ if (found) candidate = bare
483
+ else {
484
+ // `PATHEXT` without `.CMD`, or a PATH that refuses the write. The
485
+ // option still works when contributed by absolute path; only its
486
+ // label carries the suffix.
487
+ restorePath()
488
+ restorePath = undefined
489
+ warn('the host cannot resolve the launcher by name; the remote ssh option keeps its .cmd suffix')
490
+ }
491
+ } catch (err) {
492
+ candidate = launcher.path
493
+ restorePath = undefined
494
+ warn(`the launcher directory cannot be added to PATH (${reason(err)}); the remote ssh option keeps its .cmd suffix`)
495
+ }
496
+ }
497
+ try {
498
+ const undo = addShellCandidate(controller, candidate)
499
+ installed = true
500
+ return () => {
501
+ if (undo) undo()
502
+ if (restorePath) restorePath()
503
+ }
504
+ } catch (err) {
505
+ // The host advertises a shell list but will not take a write.
506
+ if (restorePath) restorePath()
507
+ warn(`the terminal shell list rejected the remote shell (${reason(err)}); the remote ssh option is unavailable`)
508
+ return undefined
509
+ }
510
+ }
511
+
512
+ if (ctx && typeof ctx.effect === 'function') {
513
+ ctx.effect(publish, 'dsh-remote.terminal-shell')
514
+ } else {
515
+ publish()
516
+ }
517
+ return installed
518
+ }
package/lib/search.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // Replaces `find … -exec grep` (which needs a POSIX shell and dies on
3
3
  // Windows remotes). Walks the remote tree over SFTP readdir/stat/readFile with
4
4
  // the same bounded-parallelism + ignore rules as the mirror sync.
5
- import { joinRemotePath, shq } from './paths.js'
5
+ import { joinRemotePath, relPathUnder, shq } from './paths.js'
6
6
 
7
7
  function mapLimit(items, limit, fn) {
8
8
  const limitN = Math.max(1, Math.min(Math.floor(limit) || 1, items.length))
@@ -105,7 +105,12 @@ export async function searchTree(sftp, rootDir, opts = {}) {
105
105
  if (shouldStop() || scanned >= maxFiles) return
106
106
  if (depth <= 0) continue
107
107
  const sub = joinRemotePath(dir, name)
108
- if (isIgnored(name, true)) continue
108
+ // isIgnored receives the path RELATIVE to the search root (the same
109
+ // contract sync has), so root-anchored ignore rules — `/proc/` when the
110
+ // workspace is the filesystem root (issue #60), `Library/Caches/` in the
111
+ // default search set — actually match here; a bare basename would never
112
+ // satisfy an anchored pattern and those rules were silently dead.
113
+ if (isIgnored(relPathUnder(rootDir, sub) || name, true)) continue
109
114
  await walk(sub, depth - 1)
110
115
  }
111
116
  if (shouldStop() || scanned >= maxFiles) return
@@ -118,8 +123,8 @@ export async function searchTree(sftp, rootDir, opts = {}) {
118
123
  // Checked per entry: this is the loop that used to run for hours.
119
124
  if (shouldStop() || scanned >= maxFiles || matches.length >= maxMatches) return
120
125
  if (glob && !matchGlob(name, glob)) return
121
- if (isIgnored(name, false)) return
122
126
  const fp = joinRemotePath(dir, name)
127
+ if (isIgnored(relPathUnder(rootDir, fp) || name, false)) return
123
128
  scanned++
124
129
  let st
125
130
  try { st = await sftp.stat(fp) } catch { return }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-remote",
3
- "version": "0.8.47",
3
+ "version": "0.8.49",
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>",
@@ -1,5 +0,0 @@
1
- 'use strict';
2
-
3
- module.exports = {
4
- extends: '@mscdex/eslint-config',
5
- };
@@ -1,88 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- pull_request:
5
- push:
6
- branches: [ master ]
7
-
8
- jobs:
9
- tests-linux:
10
- runs-on: ubuntu-latest
11
- strategy:
12
- fail-fast: false
13
- matrix:
14
- node-version: [10.x, 12.x, 14.x, 16.x, 18.x, 20.x, 22.x, 24.x]
15
- steps:
16
- - uses: actions/checkout@v4
17
- with:
18
- persist-credentials: false
19
- - name: Use Node.js ${{ matrix.node-version }}
20
- uses: actions/setup-node@v4
21
- with:
22
- node-version: ${{ matrix.node-version }}
23
- - name: Check Node.js version
24
- run: node -pe process.versions
25
- - name: Install module
26
- run: npm install
27
- - name: Run tests
28
- run: npm test
29
- tests-macos-intel:
30
- runs-on: macos-15-intel
31
- strategy:
32
- fail-fast: false
33
- matrix:
34
- node-version: [10.x, 12.x, 14.x, 16.x, 18.x, 20.x, 22.x, 24.x]
35
- steps:
36
- - uses: actions/checkout@v4
37
- with:
38
- persist-credentials: false
39
- - name: Use Node.js ${{ matrix.node-version }}
40
- uses: actions/setup-node@v4
41
- with:
42
- node-version: ${{ matrix.node-version }}
43
- - name: Check Node.js version
44
- run: node -pe process.versions
45
- - name: Install module
46
- run: npm install
47
- - name: Run tests
48
- run: npm test
49
- tests-macos-arm:
50
- runs-on: macos-latest
51
- strategy:
52
- fail-fast: false
53
- matrix:
54
- node-version: [16.x, 18.x, 20.x, 22.x, 24.x]
55
- steps:
56
- - uses: actions/checkout@v4
57
- with:
58
- persist-credentials: false
59
- - name: Use Node.js ${{ matrix.node-version }}
60
- uses: actions/setup-node@v4
61
- with:
62
- node-version: ${{ matrix.node-version }}
63
- - name: Check Node.js version
64
- run: node -pe process.versions
65
- - name: Install module
66
- run: npm install
67
- - name: Run tests
68
- run: npm test
69
- tests-windows-2022:
70
- runs-on: windows-2022
71
- strategy:
72
- fail-fast: false
73
- matrix:
74
- node-version: [24.x]
75
- steps:
76
- - uses: actions/checkout@v4
77
- with:
78
- persist-credentials: false
79
- - name: Use Node.js ${{ matrix.node-version }}
80
- uses: actions/setup-node@v4
81
- with:
82
- node-version: ${{ matrix.node-version }}
83
- - name: Check Node.js version
84
- run: node -pe process.versions
85
- - name: Install module
86
- run: npm install
87
- - name: Run tests
88
- run: npm test
@@ -1,27 +0,0 @@
1
- name: lint
2
-
3
- on:
4
- pull_request:
5
- push:
6
- branches: [ master ]
7
-
8
- env:
9
- NODE_VERSION: 22.x
10
-
11
- jobs:
12
- lint-js:
13
- runs-on: ubuntu-latest
14
- steps:
15
- - uses: actions/checkout@v4
16
- with:
17
- persist-credentials: false
18
- - name: Use Node.js ${{ env.NODE_VERSION }}
19
- uses: actions/setup-node@v4
20
- with:
21
- node-version: ${{ env.NODE_VERSION }}
22
- - name: Check Node.js version
23
- run: node -pe process.versions
24
- - name: Install ESLint + ESLint configs/plugins
25
- run: npm install --only=dev
26
- - name: Lint files
27
- run: npm run lint
@@ -1,3 +0,0 @@
1
- .eslintcache
2
- **/node_modules/**
3
- /package-lock.json