dsh-remote 0.8.48 → 0.8.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/lib/client.js +100 -19
- package/lib/index.js +134 -26
- package/lib/remote-shell-child.cjs +305 -0
- package/lib/remote-shell.js +518 -0
- package/lib/sshconfig.js +256 -15
- package/package.json +1 -2
- package/node_modules/buildcheck/.eslintrc.js +0 -5
- package/node_modules/buildcheck/.github/workflows/ci.yml +0 -88
- package/node_modules/buildcheck/.github/workflows/lint.yml +0 -27
- package/node_modules/buildcheck/.npmignore +0 -3
- package/node_modules/buildcheck/LICENSE +0 -19
- package/node_modules/buildcheck/README.md +0 -106
- package/node_modules/buildcheck/deps/Find-VisualStudio.cs +0 -250
- package/node_modules/buildcheck/lib/findvs.js +0 -361
- package/node_modules/buildcheck/lib/index.js +0 -738
- package/node_modules/buildcheck/package.json +0 -36
- package/node_modules/buildcheck/test/test.js +0 -8
- package/node_modules/cpu-features/.eslintrc.js +0 -5
- package/node_modules/cpu-features/.github/workflows/ci.yml +0 -74
- package/node_modules/cpu-features/.github/workflows/lint.yml +0 -27
- package/node_modules/cpu-features/LICENSE +0 -19
- package/node_modules/cpu-features/README.md +0 -59
- package/node_modules/cpu-features/binding.gyp +0 -16
- package/node_modules/cpu-features/buildcheck.js +0 -32
- package/node_modules/cpu-features/deps/cpu_features/.clang-format +0 -4
- package/node_modules/cpu-features/deps/cpu_features/.dockerignore +0 -31
- package/node_modules/cpu-features/deps/cpu_features/.github/workflows/Dockerfile +0 -5
- package/node_modules/cpu-features/deps/cpu_features/.github/workflows/aarch64_linux_cmake.yml +0 -30
- package/node_modules/cpu-features/deps/cpu_features/.github/workflows/amd64_freebsd_cmake.yml +0 -22
- package/node_modules/cpu-features/deps/cpu_features/.github/workflows/amd64_linux_bazel.yml +0 -26
- package/node_modules/cpu-features/deps/cpu_features/.github/workflows/amd64_linux_cmake.yml +0 -31
- package/node_modules/cpu-features/deps/cpu_features/.github/workflows/amd64_macos_cmake.yml +0 -43
- package/node_modules/cpu-features/deps/cpu_features/.github/workflows/amd64_windows_cmake.yml +0 -25
- package/node_modules/cpu-features/deps/cpu_features/.github/workflows/arm_linux_cmake.yml +0 -31
- package/node_modules/cpu-features/deps/cpu_features/.github/workflows/clang_format.yml +0 -24
- package/node_modules/cpu-features/deps/cpu_features/.github/workflows/mips_linux_cmake.yml +0 -30
- package/node_modules/cpu-features/deps/cpu_features/.github/workflows/power_linux_cmake.yml +0 -29
- package/node_modules/cpu-features/deps/cpu_features/.github/workflows/riscv_linux_cmake.yml +0 -28
- package/node_modules/cpu-features/deps/cpu_features/.github/workflows/s390x_linux_cmake.yml +0 -27
- package/node_modules/cpu-features/deps/cpu_features/.grenrc.yml +0 -21
- package/node_modules/cpu-features/deps/cpu_features/BUILD.bazel +0 -329
- package/node_modules/cpu-features/deps/cpu_features/CMakeLists.txt +0 -261
- package/node_modules/cpu-features/deps/cpu_features/CONTRIBUTING.md +0 -23
- package/node_modules/cpu-features/deps/cpu_features/LICENSE +0 -230
- package/node_modules/cpu-features/deps/cpu_features/README.md +0 -272
- package/node_modules/cpu-features/deps/cpu_features/WORKSPACE +0 -19
- package/node_modules/cpu-features/deps/cpu_features/bazel/ci/README.md +0 -5
- package/node_modules/cpu-features/deps/cpu_features/bazel/platforms.bzl +0 -11
- package/node_modules/cpu-features/deps/cpu_features/cmake/CpuFeaturesConfig.cmake.in +0 -3
- package/node_modules/cpu-features/deps/cpu_features/cmake/CpuFeaturesNdkCompatConfig.cmake.in +0 -3
- package/node_modules/cpu-features/deps/cpu_features/cmake/README.md +0 -30
- package/node_modules/cpu-features/deps/cpu_features/cmake/ci/Makefile +0 -252
- package/node_modules/cpu-features/deps/cpu_features/cmake/ci/README.md +0 -40
- package/node_modules/cpu-features/deps/cpu_features/cmake/ci/doc/docker.dot +0 -64
- package/node_modules/cpu-features/deps/cpu_features/cmake/ci/doc/docker.svg +0 -312
- package/node_modules/cpu-features/deps/cpu_features/cmake/ci/doc/generate_image.sh +0 -7
- package/node_modules/cpu-features/deps/cpu_features/cmake/ci/docker/amd64/Dockerfile +0 -48
- package/node_modules/cpu-features/deps/cpu_features/cmake/ci/docker/toolchain/Dockerfile +0 -34
- package/node_modules/cpu-features/deps/cpu_features/cmake/ci/sample/CMakeLists.txt +0 -22
- package/node_modules/cpu-features/deps/cpu_features/cmake/ci/sample/main.cpp +0 -11
- package/node_modules/cpu-features/deps/cpu_features/cmake/ci/vagrant/freebsd/Vagrantfile +0 -107
- package/node_modules/cpu-features/deps/cpu_features/cmake/googletest.CMakeLists.txt.in +0 -15
- package/node_modules/cpu-features/deps/cpu_features/cpu_features.gyp +0 -105
- package/node_modules/cpu-features/deps/cpu_features/include/cpu_features_cache_info.h +0 -54
- package/node_modules/cpu-features/deps/cpu_features/include/cpu_features_macros.h +0 -384
- package/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_aarch64.h +0 -259
- package/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_arm.h +0 -121
- package/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_mips.h +0 -74
- package/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_ppc.h +0 -149
- package/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_riscv.h +0 -72
- package/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_s390x.h +0 -108
- package/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_x86.h +0 -288
- package/node_modules/cpu-features/deps/cpu_features/include/internal/bit_utils.h +0 -40
- package/node_modules/cpu-features/deps/cpu_features/include/internal/cpuid_x86.h +0 -37
- package/node_modules/cpu-features/deps/cpu_features/include/internal/filesystem.h +0 -39
- package/node_modules/cpu-features/deps/cpu_features/include/internal/hwcaps.h +0 -240
- package/node_modules/cpu-features/deps/cpu_features/include/internal/stack_line_reader.h +0 -49
- package/node_modules/cpu-features/deps/cpu_features/include/internal/string_view.h +0 -110
- package/node_modules/cpu-features/deps/cpu_features/include/internal/windows_utils.h +0 -70
- package/node_modules/cpu-features/deps/cpu_features/ndk_compat/CMakeLists.txt +0 -60
- package/node_modules/cpu-features/deps/cpu_features/ndk_compat/README.md +0 -4
- package/node_modules/cpu-features/deps/cpu_features/ndk_compat/cpu-features.c +0 -205
- package/node_modules/cpu-features/deps/cpu_features/ndk_compat/cpu-features.h +0 -320
- package/node_modules/cpu-features/deps/cpu_features/ndk_compat/ndk-compat-test.c +0 -12
- package/node_modules/cpu-features/deps/cpu_features/patches/0001-Add-Apple-Silicon-Support.patch +0 -456
- package/node_modules/cpu-features/deps/cpu_features/scripts/generate_badges.d +0 -165
- package/node_modules/cpu-features/deps/cpu_features/scripts/make_release.sh +0 -75
- package/node_modules/cpu-features/deps/cpu_features/scripts/run_integration.sh +0 -490
- package/node_modules/cpu-features/deps/cpu_features/scripts/test_integration.sh +0 -79
- package/node_modules/cpu-features/deps/cpu_features/src/copy.inl +0 -19
- package/node_modules/cpu-features/deps/cpu_features/src/define_introspection.inl +0 -86
- package/node_modules/cpu-features/deps/cpu_features/src/define_introspection_and_hwcaps.inl +0 -26
- package/node_modules/cpu-features/deps/cpu_features/src/equals.inl +0 -22
- package/node_modules/cpu-features/deps/cpu_features/src/filesystem.c +0 -62
- package/node_modules/cpu-features/deps/cpu_features/src/hwcaps.c +0 -169
- package/node_modules/cpu-features/deps/cpu_features/src/impl_aarch64__base_implementation.inl +0 -88
- package/node_modules/cpu-features/deps/cpu_features/src/impl_aarch64_linux_or_android.c +0 -79
- package/node_modules/cpu-features/deps/cpu_features/src/impl_aarch64_macos_or_iphone.c +0 -82
- package/node_modules/cpu-features/deps/cpu_features/src/impl_aarch64_windows.c +0 -138
- package/node_modules/cpu-features/deps/cpu_features/src/impl_arm_linux_or_android.c +0 -212
- package/node_modules/cpu-features/deps/cpu_features/src/impl_mips_linux_or_android.c +0 -93
- package/node_modules/cpu-features/deps/cpu_features/src/impl_ppc_linux.c +0 -163
- package/node_modules/cpu-features/deps/cpu_features/src/impl_riscv_linux.c +0 -111
- package/node_modules/cpu-features/deps/cpu_features/src/impl_s390x_linux.c +0 -120
- package/node_modules/cpu-features/deps/cpu_features/src/impl_x86__base_implementation.inl +0 -2055
- package/node_modules/cpu-features/deps/cpu_features/src/impl_x86_freebsd.c +0 -68
- package/node_modules/cpu-features/deps/cpu_features/src/impl_x86_linux_or_android.c +0 -58
- package/node_modules/cpu-features/deps/cpu_features/src/impl_x86_macos.c +0 -57
- package/node_modules/cpu-features/deps/cpu_features/src/impl_x86_windows.c +0 -58
- package/node_modules/cpu-features/deps/cpu_features/src/stack_line_reader.c +0 -132
- package/node_modules/cpu-features/deps/cpu_features/src/string_view.c +0 -192
- package/node_modules/cpu-features/deps/cpu_features/src/utils/list_cpu_features.c +0 -461
- package/node_modules/cpu-features/deps/cpu_features/test/CMakeLists.txt +0 -114
- package/node_modules/cpu-features/deps/cpu_features/test/bit_utils_test.cc +0 -53
- package/node_modules/cpu-features/deps/cpu_features/test/cpuinfo_aarch64_test.cc +0 -396
- package/node_modules/cpu-features/deps/cpu_features/test/cpuinfo_arm_test.cc +0 -384
- package/node_modules/cpu-features/deps/cpu_features/test/cpuinfo_mips_test.cc +0 -176
- package/node_modules/cpu-features/deps/cpu_features/test/cpuinfo_ppc_test.cc +0 -131
- package/node_modules/cpu-features/deps/cpu_features/test/cpuinfo_riscv_test.cc +0 -180
- package/node_modules/cpu-features/deps/cpu_features/test/cpuinfo_s390x_test.cc +0 -82
- package/node_modules/cpu-features/deps/cpu_features/test/cpuinfo_x86_test.cc +0 -1729
- package/node_modules/cpu-features/deps/cpu_features/test/filesystem_for_testing.cc +0 -103
- package/node_modules/cpu-features/deps/cpu_features/test/filesystem_for_testing.h +0 -61
- package/node_modules/cpu-features/deps/cpu_features/test/hwcaps_for_testing.cc +0 -52
- package/node_modules/cpu-features/deps/cpu_features/test/hwcaps_for_testing.h +0 -31
- package/node_modules/cpu-features/deps/cpu_features/test/stack_line_reader_test.cc +0 -132
- package/node_modules/cpu-features/deps/cpu_features/test/string_view_test.cc +0 -202
- package/node_modules/cpu-features/lib/index.js +0 -5
- package/node_modules/cpu-features/package.json +0 -40
- package/node_modules/cpu-features/src/binding.cc +0 -151
- package/node_modules/cpu-features/test/test.js +0 -12
package/README.md
CHANGED
|
@@ -53,6 +53,7 @@ DSH 的 Web 界面刻意只监听 `127.0.0.1`(CLI 为安全拒绝 `--host 0.0.
|
|
|
53
53
|
- **Windows 主机** —— 自动探测平台并定位 Git Bash,命令经 `bash -s` 走 stdin 执行,不受引号/反斜杠转义困扰(`config.shell` 可指定或设 `native` 关闭);`C:\Users\dev` 与 `/c/Users/dev` 两种写法都接受。
|
|
54
54
|
- **长任务异步化** —— `rw_sync`/`rw_push` 传 `async: true` 返回 `taskId`,可查询进度/结果/取消。
|
|
55
55
|
- **数据跟随 Harness 根目录** —— 机器清单与镜像在 `$DSH_HOME/remote-workspaces`;0.6 之前的数据首次启动自动迁移。
|
|
56
|
+
- **`~/.ssh/config` 是连接四字段的唯一来源**(`0.8.50+`,issue #50)—— 保存机器时把 主机/用户/端口/私钥 写入 `~/.ssh/config` 的受管块(`# dsh-remote: managed` 标记,只动自己的块;你手写的同名 Host 块绝不覆盖,会提示冲突)。之后**改这个文件立即生效**:任何 Host 块点名的主机(含字面 IP),连接都按文件解析(裸 `Host *` 是通用默认,不算点名);机器记录里的副本只做兜底,插件自己的状态(最近工作区/延迟等)仍存注册表。
|
|
56
57
|
- **不改动 `dsh-workspace` 官方代码** —— 全部作为普通插件实现。
|
|
57
58
|
|
|
58
59
|
## 安装
|
package/lib/client.js
CHANGED
|
@@ -73,6 +73,7 @@ window.__ModuleLoader__.load({
|
|
|
73
73
|
'settings.sshAliasHint': '别名模式:机器只记住「{alias}」这个名字,主机/用户/端口/私钥/跳板每次连接都从 ~/.ssh/config 实时解析(改配置立刻生效)。',
|
|
74
74
|
'settings.useSshConfig': '使用 ~/.ssh/config 别名(只存别名,不存副本)',
|
|
75
75
|
'settings.useSshConfigHint': '留空或保持默认的字段(用户 root、端口 22、私钥、跳板)将从 ~/.ssh/config 实时解析;填了非默认值则覆盖。',
|
|
76
|
+
'settings.saveWritesSshConfig': '保存时会把 主机/用户/端口/私钥 写入 ~/.ssh/config 的受管块(# dsh-remote: managed 标记);此后该文件优先——直接改它立即生效。',
|
|
76
77
|
'settings.sshResolvedTo': '→ {user}@{host}:{port}',
|
|
77
78
|
'settings.sshResolvedNone': '~/.ssh/config 中没有匹配「{alias}」的 Host 条目',
|
|
78
79
|
'settings.sshResolvedWarn': '注意:{msg}',
|
|
@@ -360,6 +361,7 @@ window.__ModuleLoader__.load({
|
|
|
360
361
|
'settings.sshAliasHint': 'Alias mode: the machine remembers only the name "{alias}"; host/user/port/key/jump host are resolved from ~/.ssh/config at every connect, so editing that file takes effect immediately.',
|
|
361
362
|
'settings.useSshConfig': 'Use a ~/.ssh/config alias (keep the alias, not a copy)',
|
|
362
363
|
'settings.useSshConfigHint': 'Fields left empty or at their default (user root, port 22, key, jump host) come from ~/.ssh/config at connect time; a non-default value overrides it.',
|
|
364
|
+
'settings.saveWritesSshConfig': 'Saving writes host/user/port/key into a managed block in ~/.ssh/config (marked # dsh-remote: managed); the file then takes precedence — edit it directly and it takes effect at once.',
|
|
363
365
|
'settings.sshResolvedTo': '→ {user}@{host}:{port}',
|
|
364
366
|
'settings.sshResolvedNone': 'no ~/.ssh/config block matches "{alias}"',
|
|
365
367
|
'settings.sshResolvedWarn': 'Heads-up: {msg}',
|
|
@@ -1269,9 +1271,12 @@ window.__ModuleLoader__.load({
|
|
|
1269
1271
|
React.createElement('div', { style: { display: 'flex', gap: 6, alignItems: 'center', flexWrap: 'wrap' } },
|
|
1270
1272
|
React.createElement('span', { style: { fontWeight: 600 } }, m.name),
|
|
1271
1273
|
React.createElement('code', { style: { fontSize: 12, opacity: 0.8 } },
|
|
1272
|
-
|
|
1273
|
-
:
|
|
1274
|
-
|
|
1274
|
+
(m.sshConfigResolved && m.sshConfigResolved.matched)
|
|
1275
|
+
// Issue #50: shown for literal machines too — a Host
|
|
1276
|
+
// block naming this machine's host now drives it.
|
|
1277
|
+
? (m.host + ' ' + tr('settings.sshResolvedTo', { user: m.sshConfigResolved.username || m.username || '?', host: m.sshConfigResolved.host, port: m.sshConfigResolved.port || m.port }))
|
|
1278
|
+
: (m.useSshConfig ? m.host : (m.username + '@' + m.host + ':' + m.port))),
|
|
1279
|
+
(m.useSshConfig || (m.sshConfigResolved && m.sshConfigResolved.matched)) ? pill('', tr('settings.badgeSshConfig')) : null,
|
|
1275
1280
|
// Issue #51: a machine on the system-OpenSSH backend behaves
|
|
1276
1281
|
// differently enough (auth from ~/.ssh/config, GSSAPI) that
|
|
1277
1282
|
// the row must say so.
|
|
@@ -1314,6 +1319,9 @@ window.__ModuleLoader__.load({
|
|
|
1314
1319
|
row(tr('settings.host'), React.createElement('input', { className: 'dsh-rw-input', value: form.host, onChange: setF('host'), placeholder: form.useSshConfig ? tr('settings.sshImport') + ' Host 别名' : 'IP or hostname', style: inputS }), 'h'),
|
|
1315
1320
|
row(tr('settings.useSshConfig'), React.createElement('input', { type: 'checkbox', checked: !!form.useSshConfig, onChange: setFb('useSshConfig') }), 'sshc'),
|
|
1316
1321
|
form.useSshConfig ? React.createElement('div', { style: { fontSize: 11, opacity: 0.8, marginBottom: 8, lineHeight: 1.5 } }, tr('settings.useSshConfigHint')) : null,
|
|
1322
|
+
// Issue #50: a literal save also touches ~/.ssh/config (managed
|
|
1323
|
+
// block) — say so where the choice is made.
|
|
1324
|
+
!form.useSshConfig ? React.createElement('div', { style: { fontSize: 11, opacity: 0.8, marginBottom: 8, lineHeight: 1.5 } }, tr('settings.saveWritesSshConfig')) : null,
|
|
1317
1325
|
form.useSshConfig ? (() => {
|
|
1318
1326
|
// Preview where the alias points, from the same resolution the
|
|
1319
1327
|
// connect path uses (the host list carries it — issue #38).
|
|
@@ -2382,8 +2390,9 @@ window.__ModuleLoader__.load({
|
|
|
2382
2390
|
}
|
|
2383
2391
|
|
|
2384
2392
|
// ── better-sidebar integration (optional) ────────────────────────────────
|
|
2393
|
+
// One descriptor only: the explorer page. The per-file viewer is a native
|
|
2394
|
+
// resource type, never a better-sidebar card (see registerSidebarIntegration).
|
|
2385
2395
|
const SIDEBAR_EXPLORER_ID = 'dsh-remote:explorer'
|
|
2386
|
-
const SIDEBAR_FILE_ID = 'dsh-remote:file'
|
|
2387
2396
|
|
|
2388
2397
|
function fetchRemoteStatus(sessionId) {
|
|
2389
2398
|
return api('GET', withSessionQuery('/dsh-remote/status', sessionId)).catch(() => null)
|
|
@@ -2989,10 +2998,27 @@ window.__ModuleLoader__.load({
|
|
|
2989
2998
|
return () => { clearTimeout(tid); document.removeEventListener('mousedown', close); host.remove() }
|
|
2990
2999
|
}, [menu])
|
|
2991
3000
|
|
|
3001
|
+
// Open one remote file. The viewer is a RESOURCE tab type: the address
|
|
3002
|
+
// names the file, and the HOST's tab registry picks the type that claims
|
|
3003
|
+
// it (ours, `dsh-resource://dsh-remote/**`). That is the same call the
|
|
3004
|
+
// native explorer makes through `onOpenFile`, so one file behaves
|
|
3005
|
+
// identically in both compositions — and it needs no better-sidebar
|
|
3006
|
+
// descriptor, whose only effect would have been a switch that unregisters
|
|
3007
|
+
// the viewer.
|
|
2992
3008
|
const openFile = (p) => {
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
3009
|
+
if (props.onOpenFile) { props.onOpenFile(p); return }
|
|
3010
|
+
const sessionId = (props.scope && props.scope.sessionId) || ''
|
|
3011
|
+
try {
|
|
3012
|
+
const right = props.ctx && props.ctx.get && props.ctx.get('sidebarRight')
|
|
3013
|
+
if (right && typeof right.openResource === 'function') {
|
|
3014
|
+
right.openResource(remoteFileAddress(sessionId, p))
|
|
3015
|
+
return
|
|
3016
|
+
}
|
|
3017
|
+
} catch (e) {
|
|
3018
|
+
console.warn('[dsh-remote] remote file open failed:', e)
|
|
3019
|
+
return
|
|
3020
|
+
}
|
|
3021
|
+
console.warn('[dsh-remote] no right-sidebar resource opener; remote file not opened')
|
|
2996
3022
|
}
|
|
2997
3023
|
|
|
2998
3024
|
const doFs = (op, payload, refreshParent) => {
|
|
@@ -3302,13 +3328,14 @@ window.__ModuleLoader__.load({
|
|
|
3302
3328
|
// Issue #32: own the expansion state (see RemoteExplorerTabOwned).
|
|
3303
3329
|
component: RemoteExplorerTabOwned,
|
|
3304
3330
|
}))
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3331
|
+
// No descriptor for the FILE viewer on purpose. It is a resource tab
|
|
3332
|
+
// type (`dsh-remote/file`, registered with `patterns` on the native
|
|
3333
|
+
// registry), not a page: it only means something once an address names
|
|
3334
|
+
// the file, so it has no card to draw and no page to open. A
|
|
3335
|
+
// descriptor here would put a card in better-sidebar's 侧边栏内容
|
|
3336
|
+
// inventory whose switch could only ever unregister the viewer — the
|
|
3337
|
+
// "switch does nothing" report this replaced. The tree hands the HOST
|
|
3338
|
+
// an address instead and the host routes it to that type (openFile).
|
|
3312
3339
|
} catch (e) {
|
|
3313
3340
|
console.warn('[dsh-remote] better-sidebar integration skipped:', e)
|
|
3314
3341
|
disposers.forEach((d) => { try { d() } catch {} })
|
|
@@ -3522,11 +3549,65 @@ window.__ModuleLoader__.load({
|
|
|
3522
3549
|
// built-in local preview. This is a warm-up, not a dependency: a failure
|
|
3523
3550
|
// just leaves the conservative behaviour in place.
|
|
3524
3551
|
try { ensureMirrors() } catch { /* the registry is best-effort */ }
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3552
|
+
// ── Who owns the 开始 card ────────────────────────────────────────────
|
|
3553
|
+
// The 远程文件 card on the right sidebar's start page (开始) is the
|
|
3554
|
+
// `guide` entry of a native tab type, and TWO providers can publish one:
|
|
3555
|
+
// this plugin (below) and dsh-better-sidebar, which registers a native
|
|
3556
|
+
// type per tab descriptor it hosts — our `dsh-remote:explorer` gets a
|
|
3557
|
+
// guide row of its own there, governed by its side-card settings switch.
|
|
3558
|
+
//
|
|
3559
|
+
// Publishing both is what made that switch look broken: turning the type
|
|
3560
|
+
// off removed better-sidebar's row and left ours, so the card never went
|
|
3561
|
+
// away. The card therefore belongs to exactly ONE provider:
|
|
3562
|
+
// · better-sidebar in the composition → ITS descriptor owns the card
|
|
3563
|
+
// (its switch IS the switch for it), and this type is registered
|
|
3564
|
+
// without a guide entry. The type and its body stay: a tab opened by
|
|
3565
|
+
// kind still renders, and nothing else about the panel changes.
|
|
3566
|
+
// · plain install → the card is ours, always, exactly as before.
|
|
3567
|
+
//
|
|
3568
|
+
// `guide` is captured at registration time and an id may not be
|
|
3569
|
+
// registered twice, so a handover means dispose-then-register inside one
|
|
3570
|
+
// synchronous block: no render can observe the gap.
|
|
3571
|
+
let betterSidebar = false
|
|
3572
|
+
let disposed = false
|
|
3573
|
+
let disposeExplorer = null
|
|
3574
|
+
let guideListed = null
|
|
3575
|
+
const syncExplorerType = () => {
|
|
3576
|
+
const withGuide = !betterSidebar
|
|
3577
|
+
if (guideListed === withGuide && disposeExplorer) return
|
|
3578
|
+
const previous = disposeExplorer
|
|
3579
|
+
disposeExplorer = null
|
|
3580
|
+
if (previous) { try { previous() } catch { /* already released */ } }
|
|
3581
|
+
disposeExplorer = tabs.register({
|
|
3582
|
+
id: NATIVE_EXPLORER, kind: NATIVE_EXPLORER,
|
|
3583
|
+
title: () => tr('explorer.tabTitle'),
|
|
3584
|
+
...(withGuide ? { guide: [{ order: 55, title: () => tr('explorer.tabTitle') }] } : {}),
|
|
3585
|
+
})
|
|
3586
|
+
guideListed = withGuide
|
|
3587
|
+
}
|
|
3588
|
+
inner.effect(() => {
|
|
3589
|
+
syncExplorerType()
|
|
3590
|
+
return () => {
|
|
3591
|
+
disposed = true
|
|
3592
|
+
const dispose = disposeExplorer
|
|
3593
|
+
disposeExplorer = null
|
|
3594
|
+
guideListed = null
|
|
3595
|
+
if (dispose) { try { dispose() } catch { /* already released */ } }
|
|
3596
|
+
}
|
|
3597
|
+
}, 'dsh-remote.native-explorer-type')
|
|
3598
|
+
// The two plugins may activate in EITHER order, so the service has to be
|
|
3599
|
+
// followed rather than read once: the card moves to better-sidebar when
|
|
3600
|
+
// it arrives, and comes back here if it goes away.
|
|
3601
|
+
inner.inject(['betterSidebar'], (withSidebar) => {
|
|
3602
|
+
betterSidebar = true
|
|
3603
|
+
try { syncExplorerType() } catch (e) { console.warn('[dsh-remote] start-page card handover skipped:', e) }
|
|
3604
|
+
if (!withSidebar || typeof withSidebar.effect !== 'function') return
|
|
3605
|
+
withSidebar.effect(() => () => {
|
|
3606
|
+
betterSidebar = false
|
|
3607
|
+
if (disposed) return
|
|
3608
|
+
try { syncExplorerType() } catch (e) { console.warn('[dsh-remote] start-page card handover skipped:', e) }
|
|
3609
|
+
}, 'dsh-remote.start-card-owner')
|
|
3610
|
+
})
|
|
3530
3611
|
inner.effect(() => tabs.register({
|
|
3531
3612
|
id: NATIVE_FILE, kind: NATIVE_FILE,
|
|
3532
3613
|
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 {
|
|
@@ -33,11 +34,12 @@ import {
|
|
|
33
34
|
import { createHostKeyGuard, isHostKeyKnown as _isHostKeyKnown } from './hostkey.js'
|
|
34
35
|
import { compileIgnore, ignorePatternsFor, DEFAULT_IGNORE } from './ignore.js'
|
|
35
36
|
import { friendlyMessage } from './errors.js'
|
|
36
|
-
import { importableEntries, sshConfigPath, readSshConfigText, resolveMachineSshConfig } from './sshconfig.js'
|
|
37
|
+
import { importableEntries, sshConfigPath, readSshConfigText, resolveMachineSshConfig, resolveLiteralSshConfig, readRawSshConfig, upsertManagedHostBlock, removeManagedHostBlock, writeSshConfig } 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)
|
|
@@ -405,25 +431,49 @@ export async function apply(ctx, config) {
|
|
|
405
431
|
* keychain lookups and the settings UI keep working.
|
|
406
432
|
*/
|
|
407
433
|
const loggedSshWarnings = new Map()
|
|
408
|
-
const
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
//
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
// Warn once per distinct warning set: this runs on every tool call, and an
|
|
416
|
-
// unmatched alias would otherwise fill the log with the same line.
|
|
417
|
-
const signature = resolved.warnings.join(' | ')
|
|
418
|
-
if (signature && loggedSshWarnings.get(resolved.alias) !== signature) {
|
|
419
|
-
loggedSshWarnings.set(resolved.alias, signature)
|
|
434
|
+
const warnSshOnce = (alias, warnings) => {
|
|
435
|
+
// Warn once per distinct warning set: withSshConfig runs on every tool
|
|
436
|
+
// call, and an unmatched alias would otherwise fill the log with the same
|
|
437
|
+
// line.
|
|
438
|
+
const signature = warnings.join(' | ')
|
|
439
|
+
if (signature && loggedSshWarnings.get(alias) !== signature) {
|
|
440
|
+
loggedSshWarnings.set(alias, signature)
|
|
420
441
|
try {
|
|
421
442
|
const logger = typeof ctx.get === 'function' ? ctx.get('logger') : undefined
|
|
422
443
|
if (logger && typeof logger.warn === 'function') {
|
|
423
|
-
logger.warn(`[dsh-remote] ssh-config alias "${
|
|
444
|
+
logger.warn(`[dsh-remote] ssh-config alias "${alias}": ${warnings.join(' ')}`)
|
|
424
445
|
}
|
|
425
446
|
} catch { /* logging is best-effort */ }
|
|
426
447
|
}
|
|
448
|
+
}
|
|
449
|
+
const withSshConfig = (m) => {
|
|
450
|
+
if (!m) return m
|
|
451
|
+
// Issue #50: ~/.ssh/config wins for ANY host it names, not only alias
|
|
452
|
+
// machines. A literal machine's stored host/user/port/key is a COPY that
|
|
453
|
+
// goes stale the moment the file is edited; when a Host block names the
|
|
454
|
+
// machine's host (a bare `Host *` does not name it), the file's values
|
|
455
|
+
// take over and the record fills the gaps.
|
|
456
|
+
if (!m.useSshConfig) {
|
|
457
|
+
const lit = resolveLiteralSshConfig(m, { text: sshConfigText() })
|
|
458
|
+
if (!lit) return m
|
|
459
|
+
warnSshOnce(lit.alias, lit.warnings)
|
|
460
|
+
return {
|
|
461
|
+
...m,
|
|
462
|
+
sshConfigMatched: true,
|
|
463
|
+
sshConfigWarnings: lit.warnings,
|
|
464
|
+
// The name the user typed, kept for the system-OpenSSH backend: ssh
|
|
465
|
+
// must match its Host blocks against exactly this (connectTargetFor).
|
|
466
|
+
sshConfigName: m.host,
|
|
467
|
+
host: lit.host || m.host,
|
|
468
|
+
port: lit.port || m.port,
|
|
469
|
+
username: lit.username || m.username,
|
|
470
|
+
privateKeyPath: lit.privateKeyPath || m.privateKeyPath,
|
|
471
|
+
proxy: lit.proxy || m.proxy,
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
const resolved = resolveMachineSshConfig(m, { text: sshConfigText() })
|
|
475
|
+
if (!resolved) return m
|
|
476
|
+
warnSshOnce(resolved.alias, resolved.warnings)
|
|
427
477
|
return {
|
|
428
478
|
...m,
|
|
429
479
|
sshAlias: resolved.alias,
|
|
@@ -457,7 +507,10 @@ export async function apply(ctx, config) {
|
|
|
457
507
|
* @returns {{host: string, port: number, username: string, privateKeyPath: string, proxy: object|undefined, aliasNative: boolean}}
|
|
458
508
|
*/
|
|
459
509
|
const connectTargetFor = (eff) => {
|
|
460
|
-
|
|
510
|
+
// Issue #50: a LITERAL machine whose host a Host block names is in the same
|
|
511
|
+
// position as an alias machine on the OpenSSH backend — ssh must match its
|
|
512
|
+
// blocks against the ORIGINAL name, so it is handed over verbatim too.
|
|
513
|
+
const aliasNative = !!(eff && isSystemOpenSshBackend(eff) && (eff.useSshConfig || eff.sshConfigMatched))
|
|
461
514
|
if (!aliasNative) {
|
|
462
515
|
return {
|
|
463
516
|
host: String((eff && eff.host) || ''),
|
|
@@ -468,7 +521,7 @@ export async function apply(ctx, config) {
|
|
|
468
521
|
aliasNative: false,
|
|
469
522
|
}
|
|
470
523
|
}
|
|
471
|
-
const alias = String((eff && (eff.sshAlias || eff.host)) || '')
|
|
524
|
+
const alias = String((eff && (eff.sshAlias || eff.sshConfigName || eff.host)) || '')
|
|
472
525
|
return {
|
|
473
526
|
// The alias itself; ssh expands HostName/User/Port/IdentityFile and every
|
|
474
527
|
// other directive while matching that Host block.
|
|
@@ -485,13 +538,14 @@ export async function apply(ctx, config) {
|
|
|
485
538
|
const aliasOf = (m) => (m && m.sshAlias) || (m && m.useSshConfig ? String(m.host || '') : '')
|
|
486
539
|
|
|
487
540
|
/** Machines as the settings UI sees them: secrets stripped, plus the
|
|
488
|
-
* ~/.ssh/config resolution of an alias machine
|
|
489
|
-
*
|
|
490
|
-
*
|
|
541
|
+
* ~/.ssh/config resolution of an alias machine (issue #38) — and, since
|
|
542
|
+
* issue #50, of a LITERAL machine whose host a Host block names, so the row
|
|
543
|
+
* can show that the file now drives it. */
|
|
491
544
|
const machinesForClient = () => machines.map((m) => ({
|
|
492
545
|
...sanitizeMachine(m),
|
|
493
|
-
|
|
494
|
-
|
|
546
|
+
sshConfigResolved: m.useSshConfig
|
|
547
|
+
? resolveMachineSshConfig(m, { text: sshConfigText() })
|
|
548
|
+
: resolveLiteralSshConfig(m, { text: sshConfigText() }),
|
|
495
549
|
}))
|
|
496
550
|
|
|
497
551
|
/**
|
|
@@ -3064,12 +3118,49 @@ export async function apply(ctx, config) {
|
|
|
3064
3118
|
// alone, so a fresh registry stays "no choice yet" (not an
|
|
3065
3119
|
// explicit none) and a previously-cleared registry stays cleared.
|
|
3066
3120
|
saveMachines(machines, store.currentId, false)
|
|
3121
|
+
// Issue #50: host/user/port/key have ONE home — ~/.ssh/config.
|
|
3122
|
+
// Saving a literal machine upserts a marker-tagged Host block,
|
|
3123
|
+
// so the file and the registry can never drift; the record's
|
|
3124
|
+
// copy remains only as a fallback for a missing/renamed block.
|
|
3125
|
+
// A Host block the USER wrote for the same name is never
|
|
3126
|
+
// touched — the conflict is reported instead. Failures here are
|
|
3127
|
+
// best-effort: the registry copy is already saved.
|
|
3128
|
+
let sshConfigWarning = ''
|
|
3129
|
+
if (!rec.useSshConfig) {
|
|
3130
|
+
try {
|
|
3131
|
+
const raw = readRawSshConfig(sshConfigPath())
|
|
3132
|
+
let next = upsertManagedHostBlock(raw, {
|
|
3133
|
+
alias: rec.host,
|
|
3134
|
+
hostName: rec.host,
|
|
3135
|
+
user: rec.username,
|
|
3136
|
+
port: rec.port,
|
|
3137
|
+
identityFile: rec.privateKeyPath,
|
|
3138
|
+
})
|
|
3139
|
+
// A RENAMED machine leaves the old managed block behind, and
|
|
3140
|
+
// that block is the plugin's own artifact of the edit —
|
|
3141
|
+
// retire it. (Deleting a machine removes nothing: the Host
|
|
3142
|
+
// block is the user's ssh access, not plugin state.)
|
|
3143
|
+
if (prev && prev.host && prev.host !== rec.host && !prev.useSshConfig) {
|
|
3144
|
+
const rm = removeManagedHostBlock(next.text, prev.host)
|
|
3145
|
+
next = { status: rm.status === 'removed' && next.status === 'unchanged' ? 'updated' : next.status, text: rm.text }
|
|
3146
|
+
}
|
|
3147
|
+
if (next.status === 'conflict') {
|
|
3148
|
+
sshConfigWarning = `~/.ssh/config already names "${rec.host}" in a Host block that is not managed by dsh-remote — it was left untouched, and it now wins over the saved copy.`
|
|
3149
|
+
} else if (next.status === 'appended' || next.status === 'updated') {
|
|
3150
|
+
writeSshConfig(next.text, sshConfigPath())
|
|
3151
|
+
sshConfigCache = { text: '', at: 0 }
|
|
3152
|
+
}
|
|
3153
|
+
} catch (sshErr) {
|
|
3154
|
+
sshConfigWarning = `could not update ~/.ssh/config (${String((sshErr && sshErr.message) || sshErr)}); the registry copy was saved.`
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3067
3157
|
return sendJson(res, 200, {
|
|
3068
3158
|
ok: true,
|
|
3069
3159
|
machine: sanitizeMachine(rec),
|
|
3070
3160
|
machines: machinesForClient(),
|
|
3071
3161
|
currentId: store.currentId,
|
|
3072
3162
|
...(warning ? { warning, warningDetail } : {}),
|
|
3163
|
+
...(sshConfigWarning ? { sshConfigWarning } : {}),
|
|
3073
3164
|
})
|
|
3074
3165
|
}
|
|
3075
3166
|
if (action === 'delete') {
|
|
@@ -3128,6 +3219,12 @@ export async function apply(ctx, config) {
|
|
|
3128
3219
|
useSshConfig: true,
|
|
3129
3220
|
}, { text: sshConfigText() })
|
|
3130
3221
|
: null
|
|
3222
|
+
// Issue #50: a LITERAL host named by a Host block resolves through
|
|
3223
|
+
// the file as well — the probe must dial what a real connect would.
|
|
3224
|
+
const literalTarget = !body.useSshConfig
|
|
3225
|
+
? resolveLiteralSshConfig({ host: String(body.host || '') }, { text: sshConfigText() })
|
|
3226
|
+
: null
|
|
3227
|
+
const probeTarget = aliasTarget || literalTarget
|
|
3131
3228
|
// Issue #51: on the system-OpenSSH backend an alias is handed to `ssh`
|
|
3132
3229
|
// VERBATIM (see connectTargetFor) — resolving it here would stop the
|
|
3133
3230
|
// `Host` block from matching, so the ~/.ssh/config settings the probe
|
|
@@ -3135,13 +3232,13 @@ export async function apply(ctx, config) {
|
|
|
3135
3232
|
const probe = createPool({
|
|
3136
3233
|
...config,
|
|
3137
3234
|
backend: probeBackend,
|
|
3138
|
-
host:
|
|
3139
|
-
port:
|
|
3140
|
-
username:
|
|
3235
|
+
host: probeTarget && !probeIsOpenssh ? probeTarget.host : String(body.host || config.host),
|
|
3236
|
+
port: (probeTarget && !probeIsOpenssh && probeTarget.port) ? probeTarget.port : (Number(body.port) || config.port),
|
|
3237
|
+
username: (probeTarget && !probeIsOpenssh && probeTarget.username) ? probeTarget.username : String(body.username || config.username),
|
|
3141
3238
|
password: String(body.password || ''),
|
|
3142
|
-
privateKeyPath: (
|
|
3239
|
+
privateKeyPath: (probeTarget && !probeIsOpenssh && probeTarget.privateKeyPath) ? probeTarget.privateKeyPath : String(body.privateKeyPath || config.privateKeyPath),
|
|
3143
3240
|
passphrase: String(body.passphrase || ''),
|
|
3144
|
-
proxy: (
|
|
3241
|
+
proxy: (probeTarget && !probeIsOpenssh && probeTarget.proxy) ? probeTarget.proxy : (body.proxy && body.proxy.host ? {
|
|
3145
3242
|
host: String(body.proxy.host),
|
|
3146
3243
|
port: Number(body.proxy.port) || 22,
|
|
3147
3244
|
username: String(body.proxy.username || ''),
|
|
@@ -3731,6 +3828,17 @@ export async function apply(ctx, config) {
|
|
|
3731
3828
|
registerDeploySkill(skillCtx)
|
|
3732
3829
|
})
|
|
3733
3830
|
|
|
3831
|
+
// ── remote terminal: one extra shell in the host terminal's own list ──────
|
|
3832
|
+
// Additive and optional: a composition without `terminalController` simply
|
|
3833
|
+
// keeps the terminal it already had.
|
|
3834
|
+
//
|
|
3835
|
+
// The callback argument is a *new context*, not the service — reading the
|
|
3836
|
+
// service off it (as if it were the controller) is a silent no-op, because a
|
|
3837
|
+
// context has no `config.shellCandidates`.
|
|
3838
|
+
ctx.inject(['terminalController'], (inner) => {
|
|
3839
|
+
installTerminalShell(ctx, inner.get('terminalController'), remoteShellSpec())
|
|
3840
|
+
})
|
|
3841
|
+
|
|
3734
3842
|
// ── auto-update: check on load + on an interval; apply silently ───────────
|
|
3735
3843
|
// Failures are swallowed (never break the plugin). The persisted override
|
|
3736
3844
|
// (settings UI) wins over the profile-config default, which wins over
|