dsh-remote 0.8.26 → 0.8.27
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/lib/client.js +9 -1
- package/lib/index.js +39 -11
- package/lib/update.js +31 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -143,6 +143,7 @@ window.__ModuleLoader__.load({
|
|
|
143
143
|
'settings.updateApplied': '✅ 已更新到 v{to}({from} → {to}),宿主半已在线热切换。',
|
|
144
144
|
'settings.updateAppliedNoReload': '✅ 已更新到 v{to}({from} → {to})。重启 Harness 生效。',
|
|
145
145
|
'settings.updatePendingReload': '⚠️ 磁盘已是 v{disk},运行中的仍是 v{loaded};点「立即更新」或重启 Harness 完成切换。',
|
|
146
|
+
'settings.selfUpdateBlocked': 'ℹ️ 当前是 link:/开发模式安装(不在 node_modules 下),已禁止自动更新以免覆盖源码——请在你的源码仓库里更新。',
|
|
146
147
|
'settings.updateFailed': '更新失败',
|
|
147
148
|
'settings.updateFailedMsg': '更新失败: {msg}',
|
|
148
149
|
'settings.registryUnreachable': '无法连接 npm registry',
|
|
@@ -369,6 +370,7 @@ window.__ModuleLoader__.load({
|
|
|
369
370
|
'settings.updateApplied': '✅ Updated to v{to} ({from} → {to}); the host half hot-swapped in place.',
|
|
370
371
|
'settings.updateAppliedNoReload': '✅ Updated to v{to} ({from} → {to}). Restart Harness to apply.',
|
|
371
372
|
'settings.updatePendingReload': '⚠️ v{disk} is on disk but v{loaded} is still running; update again or restart Harness to switch.',
|
|
373
|
+
'settings.selfUpdateBlocked': 'ℹ️ This is a link:/dev install (not under node_modules), so self-update is disabled to avoid overwriting source — update it from its source repo.',
|
|
372
374
|
'settings.updateFailed': 'Update failed',
|
|
373
375
|
'settings.updateFailedMsg': 'Update failed: {msg}',
|
|
374
376
|
'settings.registryUnreachable': 'Cannot reach the npm registry',
|
|
@@ -820,7 +822,9 @@ window.__ModuleLoader__.load({
|
|
|
820
822
|
const [upd, setUpd] = React.useState(null) // { current, latest, updateAvailable, updateMode, updatedMarker }
|
|
821
823
|
const [updBusy, setUpdBusy] = React.useState(false)
|
|
822
824
|
const [updMsg, setUpdMsg] = React.useState('')
|
|
823
|
-
|
|
825
|
+
// Mirrors the host default (DEFAULT_UPDATE_MODE = 'auto') so the panel
|
|
826
|
+
// does not flash "manual" before the first /update-check answers.
|
|
827
|
+
const [updMode, setUpdMode] = React.useState('auto')
|
|
824
828
|
const checkUpdate = (quiet) => {
|
|
825
829
|
if (updBusy) return
|
|
826
830
|
setUpdBusy(true); setUpdMsg('')
|
|
@@ -1139,6 +1143,10 @@ window.__ModuleLoader__.load({
|
|
|
1139
1143
|
? React.createElement('div', { style: { marginTop: 6, fontSize: 12, color: T.warn } },
|
|
1140
1144
|
tr('settings.updatePendingReload', { disk: upd.disk, loaded: upd.loaded }))
|
|
1141
1145
|
: null,
|
|
1146
|
+
upd && upd.selfUpdateAllowed === false
|
|
1147
|
+
? React.createElement('div', { style: { marginTop: 6, fontSize: 12, color: T.warn } },
|
|
1148
|
+
tr('settings.selfUpdateBlocked'))
|
|
1149
|
+
: null,
|
|
1142
1150
|
updMsg ? React.createElement('div', { style: { marginTop: 6, fontSize: 12, color: updMsg.startsWith('✅') ? T.ok : 'inherit', opacity: 0.9 } }, updMsg) : null,
|
|
1143
1151
|
),
|
|
1144
1152
|
React.createElement('div', { style: { display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap', fontSize: 11, opacity: 0.65, borderTop: '1px solid ' + T.border, paddingTop: 10, marginTop: 4 } },
|
package/lib/index.js
CHANGED
|
@@ -41,7 +41,7 @@ import { searchRemote } from './search.js'
|
|
|
41
41
|
import { resolveMirror, poolKey, lookupSessionCwd, encodeSegmentSafe, readSessionHeaderCwd, requestSessionHint } from './binding.js'
|
|
42
42
|
import { TaskManager } from './tasks.js'
|
|
43
43
|
import { ForwardManager } from './forwards.js'
|
|
44
|
-
import { selfDir, diskVersion, LOADED_VERSION, gtVersion, fetchLatestVersion, applyUpdate, persistUpdateMode, readUpdateMode, reloadSelf, scheduleSelfReload } from './update.js'
|
|
44
|
+
import { selfDir, diskVersion, LOADED_VERSION, gtVersion, fetchLatestVersion, applyUpdate, persistUpdateMode, readUpdateMode, reloadSelf, scheduleSelfReload, isInstalledCopy } from './update.js'
|
|
45
45
|
import { sendHeartbeat, pseudonym, heartbeatUrl, hasPersistedId, installIdPath } from './telemetry.js'
|
|
46
46
|
import { loadMachines as _loadMachines, saveMachines as _saveMachines, sanitizeMachine as _sanitizeMachine, applyMachine as _applyMachine, machineId as _machineId } from './registry.js'
|
|
47
47
|
import { registerHttpTransports } from './http-transport.js'
|
|
@@ -51,6 +51,16 @@ import { listDirStructured, removeRemoteTree } from './remote-fs.js'
|
|
|
51
51
|
|
|
52
52
|
export const name = 'dsh-remote'
|
|
53
53
|
|
|
54
|
+
/** Update mode used when neither the persisted `update-mode` file nor the
|
|
55
|
+
* profile config supplies one.
|
|
56
|
+
*
|
|
57
|
+
* Kept as one exported constant on purpose: the value appears in the schema
|
|
58
|
+
* default, in the `/update-check` response and in the auto-update gate, and
|
|
59
|
+
* three hard-coded copies drift (the first version of this change left two
|
|
60
|
+
* `|| 'manual'` fallbacks that quietly disagreed with the new schema default).
|
|
61
|
+
* Changing the default must change exactly one place. */
|
|
62
|
+
export const DEFAULT_UPDATE_MODE = 'auto'
|
|
63
|
+
|
|
54
64
|
// Web and Desktop share the SSH tools; each optional UI transport is attached
|
|
55
65
|
// reactively below, so service startup order does not drop the JSON routes.
|
|
56
66
|
export const inject = ['tools', 'systemPrompt']
|
|
@@ -124,11 +134,18 @@ export const Config = z.object({
|
|
|
124
134
|
/** Wall-clock budget (ms) for one remote `@` index pass; on expiry the
|
|
125
135
|
* partial index answers rather than making the caret wait. */
|
|
126
136
|
fileReferenceTimeoutMs: z.number().step(1).min(200).default(4000),
|
|
127
|
-
/** Update mode: `
|
|
128
|
-
*
|
|
129
|
-
* `
|
|
130
|
-
*
|
|
131
|
-
|
|
137
|
+
/** Update mode: `auto` (default) checks on load and periodically, applies a
|
|
138
|
+
* newer npm release automatically, then hot-swaps the host half (see
|
|
139
|
+
* `updateAutoReload`); `manual` only checks when the user asks; `off` disables
|
|
140
|
+
* version checks entirely. (schemastery 3.18 has no .enum — keep string and
|
|
141
|
+
* validate in code.)
|
|
142
|
+
*
|
|
143
|
+
* `auto` became the default in 0.8.27. It could not have been a safe default
|
|
144
|
+
* before 0.8.24: an update only replaced files on disk and needed a restart,
|
|
145
|
+
* which left users running a new browser half against an old host half with no
|
|
146
|
+
* visible signal. 0.8.24 added the hot swap (and `pendingReload` when it is
|
|
147
|
+
* turned off), so an unattended update now actually takes effect. */
|
|
148
|
+
updateMode: z.string().default(DEFAULT_UPDATE_MODE),
|
|
132
149
|
/** How often (ms) auto mode checks the npm registry for a newer release. */
|
|
133
150
|
updateCheckIntervalMs: z.number().step(1).min(60000).default(6 * 3600 * 1000),
|
|
134
151
|
/** Whether a landed update also hot-swaps the running host half (default
|
|
@@ -2789,14 +2806,21 @@ export async function apply(ctx, config) {
|
|
|
2789
2806
|
// They differ between an applied update and its hot swap (or a boot).
|
|
2790
2807
|
const loaded = LOADED_VERSION
|
|
2791
2808
|
const disk = diskVersion()
|
|
2792
|
-
|
|
2809
|
+
// Three-layer precedence: the persisted `update-mode` override (settings
|
|
2810
|
+
// UI) beats the profile config, which beats DEFAULT_UPDATE_MODE. The
|
|
2811
|
+
// fallback must not hard-code a second default here, or this path would
|
|
2812
|
+
// silently disagree with the schema default.
|
|
2813
|
+
const rawMode = readUpdateMode() || config.updateMode || DEFAULT_UPDATE_MODE
|
|
2793
2814
|
const base = {
|
|
2794
2815
|
current: loaded,
|
|
2795
2816
|
loaded,
|
|
2796
2817
|
disk,
|
|
2797
2818
|
pendingReload: gtVersion(disk, loaded),
|
|
2798
|
-
updateMode: ['manual', 'auto', 'off'].includes(rawMode) ? rawMode :
|
|
2819
|
+
updateMode: ['manual', 'auto', 'off'].includes(rawMode) ? rawMode : DEFAULT_UPDATE_MODE,
|
|
2799
2820
|
updatedMarker: existsSync(path.join(selfDir(), '.dsh-remote-updated')),
|
|
2821
|
+
// Exposed so the settings page can explain why `auto` may not act:
|
|
2822
|
+
// a link:/dev install must not be overwritten by an npm publish.
|
|
2823
|
+
selfUpdateAllowed: isInstalledCopy(),
|
|
2800
2824
|
}
|
|
2801
2825
|
const latest = await fetchLatestVersion()
|
|
2802
2826
|
if (latest === null) return sendJson(res, 200, { ok: false, ...base, error: '无法连接 npm registry' })
|
|
@@ -2875,9 +2899,10 @@ export async function apply(ctx, config) {
|
|
|
2875
2899
|
|
|
2876
2900
|
// ── auto-update: check on load + on an interval; apply silently ───────────
|
|
2877
2901
|
// Failures are swallowed (never break the plugin). The persisted override
|
|
2878
|
-
// (settings UI) wins over the profile-config default
|
|
2879
|
-
|
|
2880
|
-
const
|
|
2902
|
+
// (settings UI) wins over the profile-config default, which wins over
|
|
2903
|
+
// DEFAULT_UPDATE_MODE — the single source of truth for the fallback.
|
|
2904
|
+
const rawMode = readUpdateMode() || config.updateMode || DEFAULT_UPDATE_MODE
|
|
2905
|
+
const effectiveUpdateMode = ['manual', 'auto', 'off'].includes(rawMode) ? rawMode : DEFAULT_UPDATE_MODE
|
|
2881
2906
|
if (effectiveUpdateMode === 'auto') {
|
|
2882
2907
|
const checkAndApply = async () => {
|
|
2883
2908
|
const latest = await fetchLatestVersion()
|
|
@@ -2892,6 +2917,9 @@ export async function apply(ctx, config) {
|
|
|
2892
2917
|
try {
|
|
2893
2918
|
await applyUpdate(latest)
|
|
2894
2919
|
} catch {
|
|
2920
|
+
// 常见且预期的失败:link:/开发模式安装时 applyUpdate 会拒绝
|
|
2921
|
+
// (避免覆盖源码仓库)。这不是异常,静默即可——那类用户在自己的
|
|
2922
|
+
// 仓库里更新。
|
|
2895
2923
|
return
|
|
2896
2924
|
}
|
|
2897
2925
|
// Landed: swap the running host half too, otherwise the code being served
|
package/lib/update.js
CHANGED
|
@@ -39,6 +39,27 @@ export function selfDir() {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* 本插件是否装在 `node_modules` 之下(即真正的"安装副本")。
|
|
44
|
+
*
|
|
45
|
+
* ★ 为什么必须有这道判断(实测依据,不是防御性猜测):
|
|
46
|
+
* `selfDir()` 基于 `import.meta.url`,而 **Node 的 ESM 解析会对符号链接做
|
|
47
|
+
* realpath**(已实测:经由 symlink 导入时 `import.meta.url` 报告的是
|
|
48
|
+
* `/real/mod.mjs` 而不是 `/link/mod.mjs`)。于是当插件以 `link:` / 开发模式
|
|
49
|
+
* 安装时,`selfDir()` 指向的是**用户的源码仓库**,而不是安装副本。
|
|
50
|
+
* 一旦 `auto` 更新落地,`applyUpdate` 就会拿 npm 包覆盖那份源码
|
|
51
|
+
* (丢改动、脏工作树)。默认改成 `auto` 之后这个风险从"手点才触发"
|
|
52
|
+
* 变成"自动发生",所以在此拦下:不在 node_modules 下就不自更新。
|
|
53
|
+
*
|
|
54
|
+
* 代价:少数把插件放在非标准路径的部署不会被自动更新——他们仍可手动更新。
|
|
55
|
+
* 这个方向的取舍是正确的:宁可少更新,也不能悄悄毁掉源码。
|
|
56
|
+
* @param dir - 要判定的目录(默认本插件的 selfDir())。
|
|
57
|
+
*/
|
|
58
|
+
export function isInstalledCopy(dir = selfDir()) {
|
|
59
|
+
const normalized = String(dir).split(path.sep).join('/').toLowerCase()
|
|
60
|
+
return normalized.includes('/node_modules/')
|
|
61
|
+
}
|
|
62
|
+
|
|
42
63
|
/** Read the installed package.json version; "0.0.0" when unreadable. */
|
|
43
64
|
export function readVersion() {
|
|
44
65
|
try {
|
|
@@ -77,6 +98,9 @@ export async function fetchLatestVersion(timeoutMs = 8000) {
|
|
|
77
98
|
try {
|
|
78
99
|
const controller = new AbortController()
|
|
79
100
|
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
101
|
+
// 纯看门狗:调用方并不 await 它,所以必须 unref,否则这一段超时窗口
|
|
102
|
+
// 会拖住事件循环、让本该退出的短命进程(脚本、测试)多挂数秒。
|
|
103
|
+
if (typeof timer.unref === 'function') timer.unref()
|
|
80
104
|
try {
|
|
81
105
|
const res = await fetch(REGISTRY_URL, { signal: controller.signal, headers: { accept: 'application/json' } })
|
|
82
106
|
if (!res.ok) return null
|
|
@@ -117,6 +141,13 @@ function parseTar(buf) {
|
|
|
117
141
|
* @param targetVersion - exact npm version to install (e.g. "0.8.0").
|
|
118
142
|
*/
|
|
119
143
|
export async function applyUpdate(targetVersion, options = {}) {
|
|
144
|
+
// 拒绝对非安装副本自更新:`auto` 默认开启后,一次自动更新就会覆盖
|
|
145
|
+
// `selfDir()` 指向的目录,而以 link:/开发模式安装时那正是用户的源码仓库
|
|
146
|
+
// (见 isInstalledCopy 的实测说明)。测试用 options.dir 指向临时目录,
|
|
147
|
+
// 因此不受此限(options.dir 存在即视为显式指定,跳过检查)。
|
|
148
|
+
if (!options.dir && !isInstalledCopy()) {
|
|
149
|
+
throw new Error('refusing to self-update: this copy is not under node_modules (link/dev install) — update it from its source repo instead')
|
|
150
|
+
}
|
|
120
151
|
const tarballUrl = `https://registry.npmjs.org/${NPM_PACKAGE}/-/${NPM_PACKAGE}-${targetVersion}.tgz`
|
|
121
152
|
const dir = options.dir || selfDir()
|
|
122
153
|
const doFetch = options.fetchImpl || fetch
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-remote",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.27",
|
|
4
4
|
"description": "Remote-work assistant for DeepSeek Harness: connect SSH (password/key/agent/keyboard-interactive/proxy jump), pick a remote workspace, operate on it with 20 rw_* tools (incl. rw_edit/rw_stat/rw_mkdir/rw_remove/rw_move/rw_forward), conflict-aware mirror sync, port forwarding, optional side-bar remote file editing, audit log, keychain passwords.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "flymysql <flyphp@outlook.com>",
|