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
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/ignore.js CHANGED
@@ -139,6 +139,52 @@ export const DEFAULT_SEARCH_IGNORE = [
139
139
  'Library/Caches/',
140
140
  ]
141
141
 
142
+ /**
143
+ * Whether a normalized remote path is a FILESYSTEM ROOT: POSIX `/`, a Windows
144
+ * drive root (`D:\`), or a UNC share root (`\\server\share`).
145
+ *
146
+ * Root workspaces are legal (issue #60) but dangerous to walk naively: `/proc`
147
+ * and `/sys` are synthetic (new pids appear between readdirs, `/proc/kcore`
148
+ * advertises 128 TB), `/dev` holds never-ending character devices, and
149
+ * `/var/lib/docker` is tens of GB of image layers. ROOT_IGNORE exists so a
150
+ * root workspace skips exactly those trees.
151
+ */
152
+ export function isFilesystemRoot(p) {
153
+ const s = String(p || '')
154
+ return s === '/' || /^[a-zA-Z]:\\?$/.test(s) || /^\\\\[^\\]+\\[^\\]+$/.test(s)
155
+ }
156
+
157
+ /**
158
+ * Patterns merged in ONLY when the workspace is the filesystem root
159
+ * (issue #60). Every entry is root-anchored AND directory-only, so even when
160
+ * merged they can only match at the workspace top level: `/` skips `/proc`,
161
+ * while a hypothetical `/home/u/proc` under a non-root workspace is ordinary
162
+ * content (and non-root workspaces never receive these patterns at all, so
163
+ * their behaviour is byte-identical to before).
164
+ */
165
+ export const ROOT_IGNORE = [
166
+ '/proc/',
167
+ '/sys/',
168
+ '/dev/',
169
+ '/run/',
170
+ '/boot/',
171
+ '/var/lib/docker/',
172
+ ]
173
+
174
+ /**
175
+ * Effective ignore patterns for a workspace rooted at `root`:
176
+ * the defaults, plus ROOT_IGNORE only for a root workspace, plus the user's
177
+ * own `.dsh-remote-ignore` lines (always last, they stack on top).
178
+ *
179
+ * @param {string} root - workspace root as passed to sync/push/search.
180
+ * @param {{search?: boolean, fromFile?: string[]}} [opts]
181
+ */
182
+ export function ignorePatternsFor(root, { search = false, fromFile = [] } = {}) {
183
+ const base = search ? DEFAULT_SEARCH_IGNORE : DEFAULT_IGNORE
184
+ const extra = isFilesystemRoot(root) ? ROOT_IGNORE : []
185
+ return base.concat(extra, fromFile)
186
+ }
187
+
142
188
  /**
143
189
  * Load the user ignore file (gitignore syntax) and merge with defaults.
144
190
  * Returns `{ patterns, fromFile }`.
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 {
@@ -31,13 +32,14 @@ import {
31
32
  toShellPath, toDisplayPath, remotePathBase, truncate, shortHash,
32
33
  } from './paths.js'
33
34
  import { createHostKeyGuard, isHostKeyKnown as _isHostKeyKnown } from './hostkey.js'
34
- import { compileIgnore, DEFAULT_IGNORE, DEFAULT_SEARCH_IGNORE } from './ignore.js'
35
+ import { compileIgnore, ignorePatternsFor, DEFAULT_IGNORE } from './ignore.js'
35
36
  import { friendlyMessage } from './errors.js'
36
37
  import { importableEntries, sshConfigPath, readSshConfigText, resolveMachineSshConfig } from './sshconfig.js'
37
38
  import { createRemoteFileReference, installFileReferenceOverlay, DEFAULT_EXCLUDED_DIRECTORIES } from './file-reference.js'
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)
@@ -927,26 +953,21 @@ export async function apply(ctx, config) {
927
953
  // NOTE: this matcher is shared by mirror sync (rw_sync/rw_push) and search.
928
954
  // Do NOT add search-only cache excludes here: widening it would silently stop
929
955
  // syncing directories a user may rely on. Search uses searchIgnoreMatcher().
930
- const ignoreMatcher = () => {
956
+ const readUserIgnore = () => {
931
957
  try {
932
- const fromFile = readFileSync(ignoreFile(), 'utf8').split('\n').map((l) => l.trim()).filter((l) => l && !l.startsWith('#'))
933
- return compileIgnore(DEFAULT_IGNORE.concat(fromFile))
958
+ return readFileSync(ignoreFile(), 'utf8').split('\n').map((l) => l.trim()).filter((l) => l && !l.startsWith('#'))
934
959
  } catch {
935
- return compileIgnore(DEFAULT_IGNORE)
960
+ return []
936
961
  }
937
962
  }
963
+ /** Mirror-sync matcher for the workspace rooted at `root` (ROOT_IGNORE is
964
+ * merged only for a filesystem-root workspace — issue #60). */
965
+ const ignoreMatcher = (root) => compileIgnore(ignorePatternsFor(root, { fromFile: readUserIgnore() }))
938
966
  /** Search-only matcher: same as ignoreMatcher() plus machine-local cache trees
939
967
  * (`~/.npm`, `~/.cache`, …) that are huge, almost never what a human is
940
968
  * looking for, and the main cause of the issue #44 hang. Mirror sync is
941
969
  * deliberately unaffected. An explicit `path` still searches anywhere. */
942
- const searchIgnoreMatcher = () => {
943
- try {
944
- const fromFile = readFileSync(ignoreFile(), 'utf8').split('\n').map((l) => l.trim()).filter((l) => l && !l.startsWith('#'))
945
- return compileIgnore(DEFAULT_SEARCH_IGNORE.concat(fromFile))
946
- } catch {
947
- return compileIgnore(DEFAULT_SEARCH_IGNORE)
948
- }
949
- }
970
+ const searchIgnoreMatcher = (root) => compileIgnore(ignorePatternsFor(root, { search: true, fromFile: readUserIgnore() }))
950
971
 
951
972
  // ── task + forward managers ───────────────────────────────────────────────
952
973
  const tasks = new TaskManager()
@@ -991,7 +1012,7 @@ export async function apply(ctx, config) {
991
1012
  const target = poolForMachine(machine)
992
1013
  let sftp
993
1014
  try { sftp = await target.sftp() } catch { return }
994
- const matcher = ignoreMatcher()
1015
+ const matcher = ignoreMatcher(ws)
995
1016
  const state = loadSyncState(localDir)
996
1017
  const next = { ...state }
997
1018
  if (rels.includes('*')) {
@@ -1711,8 +1732,15 @@ export async function apply(ctx, config) {
1711
1732
  },
1712
1733
  output: textOut,
1713
1734
  async execute(args, exec) {
1714
- const p = normalizeRemotePath(String(args.path || ''))
1715
- if (!p || p === '/') throw new Error('rw_pick_workspace: path must be an absolute directory')
1735
+ // Blank input is the only upfront rejection: normalizeRemotePath('')
1736
+ // returns '/', so the old `!p || p === '/'` guard actually meant "no
1737
+ // root" — but a filesystem root is a legal workspace (issue #60), and
1738
+ // its synthetic-tree risk is handled by ROOT_IGNORE in sync/search.
1739
+ // Any non-blank path, root included, is answered by the real directory
1740
+ // probe below (a missing path still fails there).
1741
+ const rawPath = String(args.path || '').trim()
1742
+ if (!rawPath) throw new Error('rw_pick_workspace: path must be an absolute directory')
1743
+ const p = normalizeRemotePath(rawPath)
1716
1744
  // Picking a workspace targets the machine this session is already bound
1717
1745
  // to when it has one. A LOCAL session deliberately falls back to the
1718
1746
  // active machine here: this is the setup flow that runs right after
@@ -1755,7 +1783,7 @@ export async function apply(ctx, config) {
1755
1783
  mkdirSync(local, { recursive: true })
1756
1784
  const depth = Math.min(Math.max(Number(args.depth) || 8, 1), 16)
1757
1785
  const maxFiles = Math.min(Math.max(Number(args.maxFiles) || 2000, 1), 20000)
1758
- const isIgnored = ignoreMatcher()
1786
+ const isIgnored = ignoreMatcher(p)
1759
1787
  const body = { depth, maxFiles, dryRun: !!args.dryRun, force: !!args.force, isIgnored }
1760
1788
  const runSync = async () => {
1761
1789
  let sftp
@@ -1805,7 +1833,7 @@ export async function apply(ctx, config) {
1805
1833
  const local = b.mirrorDir || mirrorDirFor(p, b.host, b.username, b.port)
1806
1834
  if (!existsSync(local)) throw new Error(`rw_push: local mirror does not exist — run rw_sync first (${local})`)
1807
1835
  const maxFiles = Math.min(Math.max(Number(args.maxFiles) || 2000, 1), 20000)
1808
- const isIgnored = ignoreMatcher()
1836
+ const isIgnored = ignoreMatcher(p)
1809
1837
  const body = { maxFiles, dryRun: !!args.dryRun, force: !!args.force, isIgnored }
1810
1838
  const runPush = async () => {
1811
1839
  let sftp
@@ -2227,7 +2255,7 @@ export async function apply(ctx, config) {
2227
2255
  // `running` and cancel/steer did nothing (issue #44).
2228
2256
  const maxFiles = Math.min(Math.max(Number(args.maxEntries) || config.searchMaxEntries, 1), 500000)
2229
2257
  const maxDurationMs = Math.min(Math.max(Number(args.maxDurationMs) || config.searchTimeoutMs, 1000), 600000)
2230
- const matcher = searchIgnoreMatcher()
2258
+ const matcher = searchIgnoreMatcher(dir)
2231
2259
  const { matches, scanned, truncated, cancelled } = await searchRemote(b.pool, dir, {
2232
2260
  pattern,
2233
2261
  regex,
@@ -2832,8 +2860,11 @@ export async function apply(ctx, config) {
2832
2860
  if (req.method !== 'POST') return sendJson(res, 405, { error: 'method not allowed' })
2833
2861
  try {
2834
2862
  const payload = JSON.parse((await readBody(req)) || '{}')
2835
- const p = normalizeRemotePath(String(payload.path || ''))
2836
- if (!p || p === '/') return sendJson(res, 400, { error: 'path must be an absolute directory' })
2863
+ // Blank-only guard: a filesystem root is a legal workspace (issue
2864
+ // #60); the real directory probe below answers everything else.
2865
+ const rawPath = String(payload.path || '').trim()
2866
+ if (!rawPath) return sendJson(res, 400, { error: 'path must be an absolute directory' })
2867
+ const p = normalizeRemotePath(rawPath)
2837
2868
  const okDir = await isRemoteDir(p)
2838
2869
  if (!okDir) return sendJson(res, 400, { ok: false, error: `not a directory: ${p}` })
2839
2870
  persistWorkspace(p)
@@ -2852,8 +2883,11 @@ export async function apply(ctx, config) {
2852
2883
  if (req.method !== 'POST') return sendJson(res, 405, { error: 'method not allowed' })
2853
2884
  try {
2854
2885
  const payload = JSON.parse((await readBody(req)) || '{}')
2855
- const p = normalizeRemotePath(String(payload.path || ''))
2856
- if (!p || p === '/') return sendJson(res, 400, { ok: false, error: 'path must be an absolute directory' })
2886
+ // Blank-only guard: a filesystem root is a legal workspace (issue
2887
+ // #60); the real directory probe below answers everything else.
2888
+ const rawPath = String(payload.path || '').trim()
2889
+ if (!rawPath) return sendJson(res, 400, { ok: false, error: 'path must be an absolute directory' })
2890
+ const p = normalizeRemotePath(rawPath)
2857
2891
  if (!config.host) return sendJson(res, 400, { ok: false, error: 'no remote host configured/connected — connect first' })
2858
2892
  // The picker names the machine it was browsing. Re-assert it so a
2859
2893
  // switch made elsewhere between the listing and this commit cannot
@@ -3723,6 +3757,17 @@ export async function apply(ctx, config) {
3723
3757
  registerDeploySkill(skillCtx)
3724
3758
  })
3725
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
+
3726
3771
  // ── auto-update: check on load + on an interval; apply silently ───────────
3727
3772
  // Failures are swallowed (never break the plugin). The persisted override
3728
3773
  // (settings UI) wins over the profile-config default, which wins over