dsh-remote 0.8.48 → 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 (130) hide show
  1. package/lib/client.js +89 -16
  2. package/lib/index.js +37 -0
  3. package/lib/remote-shell-child.cjs +305 -0
  4. package/lib/remote-shell.js +518 -0
  5. package/package.json +1 -2
  6. package/node_modules/buildcheck/.eslintrc.js +0 -5
  7. package/node_modules/buildcheck/.github/workflows/ci.yml +0 -88
  8. package/node_modules/buildcheck/.github/workflows/lint.yml +0 -27
  9. package/node_modules/buildcheck/.npmignore +0 -3
  10. package/node_modules/buildcheck/LICENSE +0 -19
  11. package/node_modules/buildcheck/README.md +0 -106
  12. package/node_modules/buildcheck/deps/Find-VisualStudio.cs +0 -250
  13. package/node_modules/buildcheck/lib/findvs.js +0 -361
  14. package/node_modules/buildcheck/lib/index.js +0 -738
  15. package/node_modules/buildcheck/package.json +0 -36
  16. package/node_modules/buildcheck/test/test.js +0 -8
  17. package/node_modules/cpu-features/.eslintrc.js +0 -5
  18. package/node_modules/cpu-features/.github/workflows/ci.yml +0 -74
  19. package/node_modules/cpu-features/.github/workflows/lint.yml +0 -27
  20. package/node_modules/cpu-features/LICENSE +0 -19
  21. package/node_modules/cpu-features/README.md +0 -59
  22. package/node_modules/cpu-features/binding.gyp +0 -16
  23. package/node_modules/cpu-features/buildcheck.js +0 -32
  24. package/node_modules/cpu-features/deps/cpu_features/.clang-format +0 -4
  25. package/node_modules/cpu-features/deps/cpu_features/.dockerignore +0 -31
  26. package/node_modules/cpu-features/deps/cpu_features/.github/workflows/Dockerfile +0 -5
  27. package/node_modules/cpu-features/deps/cpu_features/.github/workflows/aarch64_linux_cmake.yml +0 -30
  28. package/node_modules/cpu-features/deps/cpu_features/.github/workflows/amd64_freebsd_cmake.yml +0 -22
  29. package/node_modules/cpu-features/deps/cpu_features/.github/workflows/amd64_linux_bazel.yml +0 -26
  30. package/node_modules/cpu-features/deps/cpu_features/.github/workflows/amd64_linux_cmake.yml +0 -31
  31. package/node_modules/cpu-features/deps/cpu_features/.github/workflows/amd64_macos_cmake.yml +0 -43
  32. package/node_modules/cpu-features/deps/cpu_features/.github/workflows/amd64_windows_cmake.yml +0 -25
  33. package/node_modules/cpu-features/deps/cpu_features/.github/workflows/arm_linux_cmake.yml +0 -31
  34. package/node_modules/cpu-features/deps/cpu_features/.github/workflows/clang_format.yml +0 -24
  35. package/node_modules/cpu-features/deps/cpu_features/.github/workflows/mips_linux_cmake.yml +0 -30
  36. package/node_modules/cpu-features/deps/cpu_features/.github/workflows/power_linux_cmake.yml +0 -29
  37. package/node_modules/cpu-features/deps/cpu_features/.github/workflows/riscv_linux_cmake.yml +0 -28
  38. package/node_modules/cpu-features/deps/cpu_features/.github/workflows/s390x_linux_cmake.yml +0 -27
  39. package/node_modules/cpu-features/deps/cpu_features/.grenrc.yml +0 -21
  40. package/node_modules/cpu-features/deps/cpu_features/BUILD.bazel +0 -329
  41. package/node_modules/cpu-features/deps/cpu_features/CMakeLists.txt +0 -261
  42. package/node_modules/cpu-features/deps/cpu_features/CONTRIBUTING.md +0 -23
  43. package/node_modules/cpu-features/deps/cpu_features/LICENSE +0 -230
  44. package/node_modules/cpu-features/deps/cpu_features/README.md +0 -272
  45. package/node_modules/cpu-features/deps/cpu_features/WORKSPACE +0 -19
  46. package/node_modules/cpu-features/deps/cpu_features/bazel/ci/README.md +0 -5
  47. package/node_modules/cpu-features/deps/cpu_features/bazel/platforms.bzl +0 -11
  48. package/node_modules/cpu-features/deps/cpu_features/cmake/CpuFeaturesConfig.cmake.in +0 -3
  49. package/node_modules/cpu-features/deps/cpu_features/cmake/CpuFeaturesNdkCompatConfig.cmake.in +0 -3
  50. package/node_modules/cpu-features/deps/cpu_features/cmake/README.md +0 -30
  51. package/node_modules/cpu-features/deps/cpu_features/cmake/ci/Makefile +0 -252
  52. package/node_modules/cpu-features/deps/cpu_features/cmake/ci/README.md +0 -40
  53. package/node_modules/cpu-features/deps/cpu_features/cmake/ci/doc/docker.dot +0 -64
  54. package/node_modules/cpu-features/deps/cpu_features/cmake/ci/doc/docker.svg +0 -312
  55. package/node_modules/cpu-features/deps/cpu_features/cmake/ci/doc/generate_image.sh +0 -7
  56. package/node_modules/cpu-features/deps/cpu_features/cmake/ci/docker/amd64/Dockerfile +0 -48
  57. package/node_modules/cpu-features/deps/cpu_features/cmake/ci/docker/toolchain/Dockerfile +0 -34
  58. package/node_modules/cpu-features/deps/cpu_features/cmake/ci/sample/CMakeLists.txt +0 -22
  59. package/node_modules/cpu-features/deps/cpu_features/cmake/ci/sample/main.cpp +0 -11
  60. package/node_modules/cpu-features/deps/cpu_features/cmake/ci/vagrant/freebsd/Vagrantfile +0 -107
  61. package/node_modules/cpu-features/deps/cpu_features/cmake/googletest.CMakeLists.txt.in +0 -15
  62. package/node_modules/cpu-features/deps/cpu_features/cpu_features.gyp +0 -105
  63. package/node_modules/cpu-features/deps/cpu_features/include/cpu_features_cache_info.h +0 -54
  64. package/node_modules/cpu-features/deps/cpu_features/include/cpu_features_macros.h +0 -384
  65. package/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_aarch64.h +0 -259
  66. package/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_arm.h +0 -121
  67. package/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_mips.h +0 -74
  68. package/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_ppc.h +0 -149
  69. package/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_riscv.h +0 -72
  70. package/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_s390x.h +0 -108
  71. package/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_x86.h +0 -288
  72. package/node_modules/cpu-features/deps/cpu_features/include/internal/bit_utils.h +0 -40
  73. package/node_modules/cpu-features/deps/cpu_features/include/internal/cpuid_x86.h +0 -37
  74. package/node_modules/cpu-features/deps/cpu_features/include/internal/filesystem.h +0 -39
  75. package/node_modules/cpu-features/deps/cpu_features/include/internal/hwcaps.h +0 -240
  76. package/node_modules/cpu-features/deps/cpu_features/include/internal/stack_line_reader.h +0 -49
  77. package/node_modules/cpu-features/deps/cpu_features/include/internal/string_view.h +0 -110
  78. package/node_modules/cpu-features/deps/cpu_features/include/internal/windows_utils.h +0 -70
  79. package/node_modules/cpu-features/deps/cpu_features/ndk_compat/CMakeLists.txt +0 -60
  80. package/node_modules/cpu-features/deps/cpu_features/ndk_compat/README.md +0 -4
  81. package/node_modules/cpu-features/deps/cpu_features/ndk_compat/cpu-features.c +0 -205
  82. package/node_modules/cpu-features/deps/cpu_features/ndk_compat/cpu-features.h +0 -320
  83. package/node_modules/cpu-features/deps/cpu_features/ndk_compat/ndk-compat-test.c +0 -12
  84. package/node_modules/cpu-features/deps/cpu_features/patches/0001-Add-Apple-Silicon-Support.patch +0 -456
  85. package/node_modules/cpu-features/deps/cpu_features/scripts/generate_badges.d +0 -165
  86. package/node_modules/cpu-features/deps/cpu_features/scripts/make_release.sh +0 -75
  87. package/node_modules/cpu-features/deps/cpu_features/scripts/run_integration.sh +0 -490
  88. package/node_modules/cpu-features/deps/cpu_features/scripts/test_integration.sh +0 -79
  89. package/node_modules/cpu-features/deps/cpu_features/src/copy.inl +0 -19
  90. package/node_modules/cpu-features/deps/cpu_features/src/define_introspection.inl +0 -86
  91. package/node_modules/cpu-features/deps/cpu_features/src/define_introspection_and_hwcaps.inl +0 -26
  92. package/node_modules/cpu-features/deps/cpu_features/src/equals.inl +0 -22
  93. package/node_modules/cpu-features/deps/cpu_features/src/filesystem.c +0 -62
  94. package/node_modules/cpu-features/deps/cpu_features/src/hwcaps.c +0 -169
  95. package/node_modules/cpu-features/deps/cpu_features/src/impl_aarch64__base_implementation.inl +0 -88
  96. package/node_modules/cpu-features/deps/cpu_features/src/impl_aarch64_linux_or_android.c +0 -79
  97. package/node_modules/cpu-features/deps/cpu_features/src/impl_aarch64_macos_or_iphone.c +0 -82
  98. package/node_modules/cpu-features/deps/cpu_features/src/impl_aarch64_windows.c +0 -138
  99. package/node_modules/cpu-features/deps/cpu_features/src/impl_arm_linux_or_android.c +0 -212
  100. package/node_modules/cpu-features/deps/cpu_features/src/impl_mips_linux_or_android.c +0 -93
  101. package/node_modules/cpu-features/deps/cpu_features/src/impl_ppc_linux.c +0 -163
  102. package/node_modules/cpu-features/deps/cpu_features/src/impl_riscv_linux.c +0 -111
  103. package/node_modules/cpu-features/deps/cpu_features/src/impl_s390x_linux.c +0 -120
  104. package/node_modules/cpu-features/deps/cpu_features/src/impl_x86__base_implementation.inl +0 -2055
  105. package/node_modules/cpu-features/deps/cpu_features/src/impl_x86_freebsd.c +0 -68
  106. package/node_modules/cpu-features/deps/cpu_features/src/impl_x86_linux_or_android.c +0 -58
  107. package/node_modules/cpu-features/deps/cpu_features/src/impl_x86_macos.c +0 -57
  108. package/node_modules/cpu-features/deps/cpu_features/src/impl_x86_windows.c +0 -58
  109. package/node_modules/cpu-features/deps/cpu_features/src/stack_line_reader.c +0 -132
  110. package/node_modules/cpu-features/deps/cpu_features/src/string_view.c +0 -192
  111. package/node_modules/cpu-features/deps/cpu_features/src/utils/list_cpu_features.c +0 -461
  112. package/node_modules/cpu-features/deps/cpu_features/test/CMakeLists.txt +0 -114
  113. package/node_modules/cpu-features/deps/cpu_features/test/bit_utils_test.cc +0 -53
  114. package/node_modules/cpu-features/deps/cpu_features/test/cpuinfo_aarch64_test.cc +0 -396
  115. package/node_modules/cpu-features/deps/cpu_features/test/cpuinfo_arm_test.cc +0 -384
  116. package/node_modules/cpu-features/deps/cpu_features/test/cpuinfo_mips_test.cc +0 -176
  117. package/node_modules/cpu-features/deps/cpu_features/test/cpuinfo_ppc_test.cc +0 -131
  118. package/node_modules/cpu-features/deps/cpu_features/test/cpuinfo_riscv_test.cc +0 -180
  119. package/node_modules/cpu-features/deps/cpu_features/test/cpuinfo_s390x_test.cc +0 -82
  120. package/node_modules/cpu-features/deps/cpu_features/test/cpuinfo_x86_test.cc +0 -1729
  121. package/node_modules/cpu-features/deps/cpu_features/test/filesystem_for_testing.cc +0 -103
  122. package/node_modules/cpu-features/deps/cpu_features/test/filesystem_for_testing.h +0 -61
  123. package/node_modules/cpu-features/deps/cpu_features/test/hwcaps_for_testing.cc +0 -52
  124. package/node_modules/cpu-features/deps/cpu_features/test/hwcaps_for_testing.h +0 -31
  125. package/node_modules/cpu-features/deps/cpu_features/test/stack_line_reader_test.cc +0 -132
  126. package/node_modules/cpu-features/deps/cpu_features/test/string_view_test.cc +0 -202
  127. package/node_modules/cpu-features/lib/index.js +0 -5
  128. package/node_modules/cpu-features/package.json +0 -40
  129. package/node_modules/cpu-features/src/binding.cc +0 -151
  130. package/node_modules/cpu-features/test/test.js +0 -12
package/lib/client.js CHANGED
@@ -2382,8 +2382,9 @@ window.__ModuleLoader__.load({
2382
2382
  }
2383
2383
 
2384
2384
  // ── better-sidebar integration (optional) ────────────────────────────────
2385
+ // One descriptor only: the explorer page. The per-file viewer is a native
2386
+ // resource type, never a better-sidebar card (see registerSidebarIntegration).
2385
2387
  const SIDEBAR_EXPLORER_ID = 'dsh-remote:explorer'
2386
- const SIDEBAR_FILE_ID = 'dsh-remote:file'
2387
2388
 
2388
2389
  function fetchRemoteStatus(sessionId) {
2389
2390
  return api('GET', withSessionQuery('/dsh-remote/status', sessionId)).catch(() => null)
@@ -2989,10 +2990,27 @@ window.__ModuleLoader__.load({
2989
2990
  return () => { clearTimeout(tid); document.removeEventListener('mousedown', close); host.remove() }
2990
2991
  }, [menu])
2991
2992
 
2993
+ // Open one remote file. The viewer is a RESOURCE tab type: the address
2994
+ // names the file, and the HOST's tab registry picks the type that claims
2995
+ // it (ours, `dsh-resource://dsh-remote/**`). That is the same call the
2996
+ // native explorer makes through `onOpenFile`, so one file behaves
2997
+ // identically in both compositions — and it needs no better-sidebar
2998
+ // descriptor, whose only effect would have been a switch that unregisters
2999
+ // the viewer.
2992
3000
  const openFile = (p) => {
2993
- const name = String(p).split(/[\\/]/).pop() || p
2994
- if (props.onOpenFile) props.onOpenFile(p)
2995
- else props.ctx.betterSidebar.openTab({ type: SIDEBAR_FILE_ID, title: name, path: p }, props.scope)
3001
+ if (props.onOpenFile) { props.onOpenFile(p); return }
3002
+ const sessionId = (props.scope && props.scope.sessionId) || ''
3003
+ try {
3004
+ const right = props.ctx && props.ctx.get && props.ctx.get('sidebarRight')
3005
+ if (right && typeof right.openResource === 'function') {
3006
+ right.openResource(remoteFileAddress(sessionId, p))
3007
+ return
3008
+ }
3009
+ } catch (e) {
3010
+ console.warn('[dsh-remote] remote file open failed:', e)
3011
+ return
3012
+ }
3013
+ console.warn('[dsh-remote] no right-sidebar resource opener; remote file not opened')
2996
3014
  }
2997
3015
 
2998
3016
  const doFs = (op, payload, refreshParent) => {
@@ -3302,13 +3320,14 @@ window.__ModuleLoader__.load({
3302
3320
  // Issue #32: own the expansion state (see RemoteExplorerTabOwned).
3303
3321
  component: RemoteExplorerTabOwned,
3304
3322
  }))
3305
- disposers.push(bs.registerTab({
3306
- id: SIDEBAR_FILE_ID,
3307
- title: () => tr('explorer.tabTitle'),
3308
- hidden: true, // not in the + menu; opened from the explorer tree
3309
- dedupeKey: (tab) => tab.path,
3310
- component: (props) => React.createElement(RemoteFileTab, props),
3311
- }))
3323
+ // No descriptor for the FILE viewer on purpose. It is a resource tab
3324
+ // type (`dsh-remote/file`, registered with `patterns` on the native
3325
+ // registry), not a page: it only means something once an address names
3326
+ // the file, so it has no card to draw and no page to open. A
3327
+ // descriptor here would put a card in better-sidebar's 侧边栏内容
3328
+ // inventory whose switch could only ever unregister the viewer — the
3329
+ // "switch does nothing" report this replaced. The tree hands the HOST
3330
+ // an address instead and the host routes it to that type (openFile).
3312
3331
  } catch (e) {
3313
3332
  console.warn('[dsh-remote] better-sidebar integration skipped:', e)
3314
3333
  disposers.forEach((d) => { try { d() } catch {} })
@@ -3522,11 +3541,65 @@ window.__ModuleLoader__.load({
3522
3541
  // built-in local preview. This is a warm-up, not a dependency: a failure
3523
3542
  // just leaves the conservative behaviour in place.
3524
3543
  try { ensureMirrors() } catch { /* the registry is best-effort */ }
3525
- inner.effect(() => tabs.register({
3526
- id: NATIVE_EXPLORER, kind: NATIVE_EXPLORER,
3527
- title: () => tr('explorer.tabTitle'),
3528
- guide: [{ order: 55, title: () => tr('explorer.tabTitle') }],
3529
- }), 'dsh-remote.native-explorer-type')
3544
+ // ── Who owns the 开始 card ────────────────────────────────────────────
3545
+ // The 远程文件 card on the right sidebar's start page (开始) is the
3546
+ // `guide` entry of a native tab type, and TWO providers can publish one:
3547
+ // this plugin (below) and dsh-better-sidebar, which registers a native
3548
+ // type per tab descriptor it hosts — our `dsh-remote:explorer` gets a
3549
+ // guide row of its own there, governed by its side-card settings switch.
3550
+ //
3551
+ // Publishing both is what made that switch look broken: turning the type
3552
+ // off removed better-sidebar's row and left ours, so the card never went
3553
+ // away. The card therefore belongs to exactly ONE provider:
3554
+ // · better-sidebar in the composition → ITS descriptor owns the card
3555
+ // (its switch IS the switch for it), and this type is registered
3556
+ // without a guide entry. The type and its body stay: a tab opened by
3557
+ // kind still renders, and nothing else about the panel changes.
3558
+ // · plain install → the card is ours, always, exactly as before.
3559
+ //
3560
+ // `guide` is captured at registration time and an id may not be
3561
+ // registered twice, so a handover means dispose-then-register inside one
3562
+ // synchronous block: no render can observe the gap.
3563
+ let betterSidebar = false
3564
+ let disposed = false
3565
+ let disposeExplorer = null
3566
+ let guideListed = null
3567
+ const syncExplorerType = () => {
3568
+ const withGuide = !betterSidebar
3569
+ if (guideListed === withGuide && disposeExplorer) return
3570
+ const previous = disposeExplorer
3571
+ disposeExplorer = null
3572
+ if (previous) { try { previous() } catch { /* already released */ } }
3573
+ disposeExplorer = tabs.register({
3574
+ id: NATIVE_EXPLORER, kind: NATIVE_EXPLORER,
3575
+ title: () => tr('explorer.tabTitle'),
3576
+ ...(withGuide ? { guide: [{ order: 55, title: () => tr('explorer.tabTitle') }] } : {}),
3577
+ })
3578
+ guideListed = withGuide
3579
+ }
3580
+ inner.effect(() => {
3581
+ syncExplorerType()
3582
+ return () => {
3583
+ disposed = true
3584
+ const dispose = disposeExplorer
3585
+ disposeExplorer = null
3586
+ guideListed = null
3587
+ if (dispose) { try { dispose() } catch { /* already released */ } }
3588
+ }
3589
+ }, 'dsh-remote.native-explorer-type')
3590
+ // The two plugins may activate in EITHER order, so the service has to be
3591
+ // followed rather than read once: the card moves to better-sidebar when
3592
+ // it arrives, and comes back here if it goes away.
3593
+ inner.inject(['betterSidebar'], (withSidebar) => {
3594
+ betterSidebar = true
3595
+ try { syncExplorerType() } catch (e) { console.warn('[dsh-remote] start-page card handover skipped:', e) }
3596
+ if (!withSidebar || typeof withSidebar.effect !== 'function') return
3597
+ withSidebar.effect(() => () => {
3598
+ betterSidebar = false
3599
+ if (disposed) return
3600
+ try { syncExplorerType() } catch (e) { console.warn('[dsh-remote] start-page card handover skipped:', e) }
3601
+ }, 'dsh-remote.start-card-owner')
3602
+ })
3530
3603
  inner.effect(() => tabs.register({
3531
3604
  id: NATIVE_FILE, kind: NATIVE_FILE,
3532
3605
  patterns: ['dsh-resource://dsh-remote/**'],
package/lib/index.js CHANGED
@@ -24,6 +24,7 @@ import { execFile } from 'node:child_process'
24
24
  import { readFileSync, mkdirSync, writeFileSync, existsSync, readdirSync, statSync, renameSync, copyFileSync, appendFileSync, watch, rmSync } from 'node:fs'
25
25
  import { homedir } from 'node:os'
26
26
  import path from 'node:path'
27
+ import { fileURLToPath } from 'node:url'
27
28
  import iconv from 'iconv-lite'
28
29
 
29
30
  import {
@@ -38,6 +39,7 @@ import { createRemoteFileReference, installFileReferenceOverlay, DEFAULT_EXCLUDE
38
39
  import { getSecret, deleteSecret, platformBackend, persistPassword } from './credential.js'
39
40
  import { syncTree, pushTree, loadSyncState, saveSyncState, pushOneFile } from './sync.js'
40
41
  import { searchRemote } from './search.js'
42
+ import { installTerminalShell } from './remote-shell.js'
41
43
  import { resolveMirror, poolKey, lookupSessionCwd, encodeSegmentSafe, readSessionHeaderCwd, requestSessionHint } from './binding.js'
42
44
  import { TaskManager } from './tasks.js'
43
45
  import { DeployTasks } from './deploy-tasks.js'
@@ -256,6 +258,30 @@ function mirrorRootFor(host, user, port) {
256
258
  return path.join(remoteWorkspacesRoot(), tag)
257
259
  }
258
260
 
261
+ /** Generated terminal launchers. Deliberately NOT under `remote-workspaces`: a
262
+ * launcher there would sit next to the mirrors, where anything that scans the
263
+ * root for checkouts could pick it up as one. */
264
+ function remoteTerminalRoot() {
265
+ return path.join(dshBase(), 'remote-terminal')
266
+ }
267
+
268
+ /**
269
+ * Every path the remote-shell contribution needs, resolved for this install.
270
+ *
271
+ * Split out from the registration itself so the wiring at the bottom of the
272
+ * plugin stays one readable line, and so the launcher path is a pure function
273
+ * of where the plugin lives.
274
+ *
275
+ * @returns {{ dir: string, childPath: string, machinesFile: string }}
276
+ */
277
+ function remoteShellSpec() {
278
+ return {
279
+ dir: remoteTerminalRoot(),
280
+ childPath: fileURLToPath(new URL('./remote-shell-child.cjs', import.meta.url)),
281
+ machinesFile: machinesFile(),
282
+ }
283
+ }
284
+
259
285
  /** Local mirror dir for a specific remote path (idempotent → returns same dir). */
260
286
  function mirrorDirFor(remotePath, host, user, port) {
261
287
  const base = remotePathBase(remotePath)
@@ -3731,6 +3757,17 @@ export async function apply(ctx, config) {
3731
3757
  registerDeploySkill(skillCtx)
3732
3758
  })
3733
3759
 
3760
+ // ── remote terminal: one extra shell in the host terminal's own list ──────
3761
+ // Additive and optional: a composition without `terminalController` simply
3762
+ // keeps the terminal it already had.
3763
+ //
3764
+ // The callback argument is a *new context*, not the service — reading the
3765
+ // service off it (as if it were the controller) is a silent no-op, because a
3766
+ // context has no `config.shellCandidates`.
3767
+ ctx.inject(['terminalController'], (inner) => {
3768
+ installTerminalShell(ctx, inner.get('terminalController'), remoteShellSpec())
3769
+ })
3770
+
3734
3771
  // ── auto-update: check on load + on an interval; apply silently ───────────
3735
3772
  // Failures are swallowed (never break the plugin). The persisted override
3736
3773
  // (settings UI) wins over the profile-config default, which wins over
@@ -0,0 +1,305 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * The program the host terminal runs when the user picks the "Remote SSH" shell.
4
+ *
5
+ * It is started by a generated launcher (see `remote-shell.js`) inside the
6
+ * session's working directory, resolves which remote checkout that directory
7
+ * belongs to, and then replaces itself with `ssh`. Inheriting stdio is the whole
8
+ * point: the local pty stays in charge of the screen, so resize and key handling
9
+ * keep working without this script knowing anything about terminals.
10
+ *
11
+ * Plain CommonJS on purpose: it is spawned as a standalone script, and it must
12
+ * not depend on how the plugin package itself is loaded.
13
+ */
14
+
15
+ 'use strict'
16
+
17
+ const { spawn } = require('node:child_process')
18
+ const { existsSync, readFileSync } = require('node:fs')
19
+ const path = require('node:path')
20
+
21
+ /** Name of the marker the host half writes into every local mirror root. */
22
+ const META_FILE = '.dsh-remote-meta.json'
23
+ /** How far up from the working directory a marker may be found. */
24
+ const MAX_WALK_UP = 12
25
+
26
+ /**
27
+ * Read and parse a JSON file, returning undefined for anything unusable.
28
+ * @param {string} file
29
+ * @returns {any}
30
+ */
31
+ function readJson(file) {
32
+ try {
33
+ return JSON.parse(readFileSync(file, 'utf8'))
34
+ } catch {
35
+ return undefined
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Find the mirror root that contains `cwd` by walking upwards for the marker.
41
+ * @param {string} cwd - session working directory
42
+ * @param {{ exists?: (p: string) => boolean, readJsonFile?: (p: string) => any, maxUp?: number }} deps
43
+ * @returns {{ dir: string, meta: object } | null}
44
+ */
45
+ function findMirror(cwd, deps = {}) {
46
+ const exists = deps.exists || ((p) => existsSync(p))
47
+ const read = deps.readJsonFile || readJson
48
+ const maxUp = deps.maxUp || MAX_WALK_UP
49
+ let dir = path.resolve(cwd || '.')
50
+ for (let depth = 0; depth <= maxUp; depth += 1) {
51
+ const marker = path.join(dir, META_FILE)
52
+ if (exists(marker)) {
53
+ const meta = read(marker)
54
+ if (meta && typeof meta === 'object' && meta.host) return { dir, meta }
55
+ }
56
+ const parent = path.dirname(dir)
57
+ if (parent === dir) break
58
+ dir = parent
59
+ }
60
+ return null
61
+ }
62
+
63
+ /**
64
+ * Pick the saved connection that owns a mirror.
65
+ *
66
+ * The marker records the SSH alias when the machine was saved from
67
+ * `~/.ssh/config` (issue #38), so the alias is the most precise key; otherwise
68
+ * host + user + port identify it. A missing entry is not fatal — the marker
69
+ * alone is enough to connect, and ssh will use its own defaults for the key.
70
+ *
71
+ * @param {object} meta - mirror marker
72
+ * @param {{ list?: object[] }} registry - parsed `machines.json`
73
+ * @returns {object | undefined}
74
+ */
75
+ function matchMachine(meta, registry) {
76
+ const list = registry && Array.isArray(registry.list) ? registry.list : []
77
+ const port = Number(meta.port || 22)
78
+ if (meta.alias) {
79
+ const byAlias = list.find((m) => m && m.host === meta.alias)
80
+ if (byAlias) return byAlias
81
+ }
82
+ return list.find((m) => m && m.host === meta.host
83
+ && String(m.username || '') === String(meta.username || '')
84
+ && Number(m.port || 22) === port)
85
+ }
86
+
87
+ /**
88
+ * Quote one path for a POSIX shell, single quotes only.
89
+ * @param {string} value
90
+ * @returns {string}
91
+ */
92
+ function quotePosix(value) {
93
+ return "'" + String(value).replace(/'/g, "'\\''") + "'"
94
+ }
95
+
96
+ /**
97
+ * Build the `cd` half of the remote command, keeping `~` for the *remote* shell
98
+ * to expand: the local home is not the remote home, so expanding it here would
99
+ * silently land in the wrong directory.
100
+ * @param {string} remotePath
101
+ * @returns {string}
102
+ */
103
+ function remoteCd(remotePath) {
104
+ const raw = String(remotePath || '').trim()
105
+ if (raw === '' || raw === '~') return 'cd "$HOME"'
106
+ if (raw === '~/') return 'cd "$HOME"'
107
+ if (raw.startsWith('~/')) return 'cd "$HOME"/' + quotePosix(raw.slice(2))
108
+ return 'cd ' + quotePosix(raw)
109
+ }
110
+
111
+ /**
112
+ * The remote command: land in the workspace, then hand over to a login shell.
113
+ *
114
+ * `exec` keeps the pty attached to a single interactive shell, so the exit code
115
+ * the terminal sees is the shell's own, and a deleted workspace falls back to
116
+ * `$HOME` instead of leaving a broken terminal.
117
+ *
118
+ * @param {string} remotePath
119
+ * @returns {string}
120
+ */
121
+ function remoteCommand(remotePath) {
122
+ return `${remoteCd(remotePath)} 2>/dev/null || cd "$HOME"; exec "\${SHELL:-/bin/bash}" -l`
123
+ }
124
+
125
+ /** Absolute Windows path: `D:\x`, `D:/x`, or a `\\server\share` UNC path.
126
+ * These cannot be the operand of a POSIX `cd`. */
127
+ const WINDOWS_REMOTE_PATH = /^(?:[a-zA-Z]:[\\/]|\\\\)/
128
+
129
+ /**
130
+ * Whether a remote workspace path is a Windows one.
131
+ *
132
+ * A Windows remote speaks cmd or PowerShell in its default shell, so the POSIX
133
+ * `cd … || …; exec "$SHELL"` line would fail outright there — `2>/dev/null`,
134
+ * `||` and `exec` are not cmd syntax at all. The marker records no remote
135
+ * platform, but the path form is unambiguous: `/srv/x` and `~/x` are POSIX,
136
+ * while `D:\x` and `\\server\share` are not valid POSIX paths.
137
+ *
138
+ * @param {string} p - `remotePath` from the mirror marker
139
+ * @returns {boolean}
140
+ */
141
+ function isWindowsRemotePath(p) {
142
+ return WINDOWS_REMOTE_PATH.test(String(p || '').trim())
143
+ }
144
+
145
+ /**
146
+ * Argument list (without the ssh binary) for one connection.
147
+ *
148
+ * `-tt` is mandatory: the terminal needs a tty even though a command is given,
149
+ * otherwise the shell comes up non-interactive and every key press is ignored.
150
+ * Options are pushed in order and never spliced between an `-o` and its value —
151
+ * an inserted `-J` there once reached ssh as the value of `-o`, which aborted
152
+ * every connection that used a jump host (issue #21).
153
+ *
154
+ * @param {object} target - `{ host, port, username, alias, remotePath }`
155
+ * @param {object} [machine] - saved connection, for the key and transport hints
156
+ * @returns {string[]}
157
+ */
158
+ function buildSshArgs(target, machine) {
159
+ const args = ['-tt']
160
+ const alias = target.alias || (machine && machine.useSshConfig ? machine.host : '')
161
+ if (alias) {
162
+ args.push(String(alias))
163
+ } else {
164
+ const port = Number(target.port || (machine && machine.port) || 22)
165
+ if (port !== 22) args.push('-p', String(port))
166
+ const key = machine && machine.privateKeyPath
167
+ if (key && !machine.useAgent) args.push('-i', String(key))
168
+ const user = target.username || (machine && machine.username) || ''
169
+ args.push(user ? `${user}@${target.host}` : String(target.host))
170
+ }
171
+ // A Windows workspace cannot be reached with the POSIX line: the remote's
172
+ // default shell may be cmd or PowerShell, and picking one would be a guess.
173
+ // Opening ssh with no command leaves the remote's own default shell in charge,
174
+ // so the terminal still works; only the automatic `cd` is given up.
175
+ if (!isWindowsRemotePath(target.remotePath)) args.push(remoteCommand(target.remotePath))
176
+ return args
177
+ }
178
+
179
+ /**
180
+ * Prefer the OS OpenSSH binary on Windows. A PATH lookup there can reach an
181
+ * MSYS2/git-bash `ssh`, which resolves `~/.ssh` against a different home and
182
+ * makes keys saved by the host half look missing.
183
+ * @param {string} [platform]
184
+ * @param {object} [env]
185
+ * @param {(p: string) => boolean} [exists]
186
+ * @returns {string}
187
+ */
188
+ function sshBinary(platform = process.platform, env = process.env, exists = existsSync) {
189
+ if (platform === 'win32') {
190
+ const system = env.SystemRoot || env.windir || 'C:\\Windows'
191
+ const native = path.join(system, 'System32', 'OpenSSH', 'ssh.exe')
192
+ if (exists(native)) return native
193
+ }
194
+ return 'ssh'
195
+ }
196
+
197
+ /**
198
+ * The shell to fall back to when this directory is not a remote checkout.
199
+ * @param {string} [platform]
200
+ * @param {object} [env]
201
+ * @returns {string}
202
+ */
203
+ function localShell(platform = process.platform, env = process.env) {
204
+ if (platform === 'win32') return env.ComSpec || 'cmd.exe'
205
+ return env.SHELL || '/bin/sh'
206
+ }
207
+
208
+ /**
209
+ * Parse the arguments the launcher passes.
210
+ * @param {string[]} argv
211
+ * @returns {{ machines?: string }}
212
+ */
213
+ function parseArgs(argv) {
214
+ const out = {}
215
+ for (let i = 0; i < argv.length; i += 1) {
216
+ if (argv[i] === '--machines' && argv[i + 1]) {
217
+ out.machines = argv[i + 1]
218
+ i += 1
219
+ }
220
+ }
221
+ return out
222
+ }
223
+
224
+ /**
225
+ * Resolve everything needed to connect from a working directory.
226
+ * @param {{ cwd?: string, machines?: string, exists?: Function, readJsonFile?: Function }} input
227
+ * @returns {{ target?: object, machine?: object, found: boolean }}
228
+ */
229
+ function resolveTarget(input = {}) {
230
+ const cwd = input.cwd || process.cwd()
231
+ const mirror = findMirror(cwd, input)
232
+ if (!mirror) return { found: false }
233
+ const registry = input.machines ? (input.readJsonFile || readJson)(input.machines) : undefined
234
+ const machine = matchMachine(mirror.meta, registry)
235
+ const target = {
236
+ host: mirror.meta.host,
237
+ port: mirror.meta.port || (machine && machine.port) || 22,
238
+ username: mirror.meta.username || (machine && machine.username) || '',
239
+ alias: mirror.meta.alias || '',
240
+ remotePath: mirror.meta.remotePath || (machine && machine.workspace) || '',
241
+ }
242
+ return { found: true, target, machine, dir: mirror.dir }
243
+ }
244
+
245
+ /**
246
+ * Hand the current process over to `file args…`, propagating the exit code.
247
+ * @param {string} file
248
+ * @param {string[]} args
249
+ * @returns {import('node:child_process').ChildProcess}
250
+ */
251
+ function handover(file, args) {
252
+ const child = spawn(file, args, { stdio: 'inherit' })
253
+ child.on('error', (error) => {
254
+ process.stderr.write(`[dsh-remote] cannot run ${file}: ${error && error.message}\n`)
255
+ process.exit(127)
256
+ })
257
+ child.on('exit', (code, signal) => {
258
+ if (signal) process.kill(process.pid, signal)
259
+ else process.exit(code == null ? 0 : code)
260
+ })
261
+ return child
262
+ }
263
+
264
+ /**
265
+ * Entry point: connect, or explain why a local shell was used instead.
266
+ * @param {string[]} [argv]
267
+ * @param {{ cwd?: string, platform?: string, env?: object }} [deps]
268
+ */
269
+ function main(argv = process.argv.slice(2), deps = {}) {
270
+ const platform = deps.platform || process.platform
271
+ const env = deps.env || process.env
272
+ const options = parseArgs(argv)
273
+ const resolved = resolveTarget({ cwd: deps.cwd || process.cwd(), machines: options.machines })
274
+ if (!resolved.found) {
275
+ process.stderr.write('[dsh-remote] this directory is not a remote workspace; opening a local shell.\n')
276
+ process.stderr.write('[dsh-remote] 当前目录不是远程工作区,已打开本地终端。\n')
277
+ return handover(localShell(platform, env), platform === 'win32' ? [] : ['-l'])
278
+ }
279
+ const ssh = sshBinary(platform, env)
280
+ const args = buildSshArgs(resolved.target, resolved.machine)
281
+ const user = resolved.target.username || (resolved.machine && resolved.machine.username) || ''
282
+ process.stderr.write(`[dsh-remote] connecting to ${user ? user + '@' : ''}${resolved.target.host}…\n`)
283
+ if (isWindowsRemotePath(resolved.target.remotePath)) {
284
+ process.stderr.write('[dsh-remote] the remote workspace is a Windows path; opening the remote default shell without changing directory.\n')
285
+ process.stderr.write('[dsh-remote] 远端工作区是 Windows 路径,已打开默认 shell(未自动切换目录)。\n')
286
+ }
287
+ return handover(ssh, args)
288
+ }
289
+
290
+ module.exports = {
291
+ META_FILE,
292
+ buildSshArgs,
293
+ findMirror,
294
+ isWindowsRemotePath,
295
+ localShell,
296
+ main,
297
+ matchMachine,
298
+ parseArgs,
299
+ remoteCd,
300
+ remoteCommand,
301
+ resolveTarget,
302
+ sshBinary,
303
+ }
304
+
305
+ if (require.main === module) main()