dsh-remote 0.8.25 → 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/README.md +1 -1
- package/README.zh.md +1 -1
- package/lib/client.js +9 -1
- package/lib/index.js +39 -11
- package/lib/telemetry.js +71 -21
- package/lib/update.js +31 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
[](LICENSE)
|
|
11
11
|
[](https://github.com/topics/dsh-plugin)
|
|
12
12
|
|
|
13
|
-
Maintained by [@flymysql](https://github.com/flymysql) · [Blog](https://gitpull.cn) · [Discussions](https://github.com/flymysql/dsh-remote/discussions) · [Issues](https://github.com/flymysql/dsh-remote/issues) · [中文说明](./README.zh.md)
|
|
13
|
+
Maintained by [@flymysql](https://github.com/flymysql) · [Homepage](https://flymysql.github.io/dsh-remote/) · [Usage stats](https://flymysql.github.io/dsh-remote/stats/) · [Blog](https://gitpull.cn) · [Discussions](https://github.com/flymysql/dsh-remote/discussions) · [Issues](https://github.com/flymysql/dsh-remote/issues) · [中文说明](./README.zh.md)
|
|
14
14
|
|
|
15
15
|

|
|
16
16
|
|
package/README.zh.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
# dsh-remote
|
|
6
6
|
|
|
7
|
-
由 [@flymysql](https://github.com/flymysql) 维护 · [博客](https://gitpull.cn) · [讨论区](https://github.com/flymysql/dsh-remote/discussions) · [Issue](https://github.com/flymysql/dsh-remote/issues) · [English](./README.md)
|
|
7
|
+
由 [@flymysql](https://github.com/flymysql) 维护 · [主页](https://flymysql.github.io/dsh-remote/) · [用量统计](https://flymysql.github.io/dsh-remote/stats/) · [博客](https://gitpull.cn) · [讨论区](https://github.com/flymysql/dsh-remote/discussions) · [Issue](https://github.com/flymysql/dsh-remote/issues) · [English](./README.md)
|
|
8
8
|
|
|
9
9
|
## 官方 Desktop 兼容适配(实验性,尚未发布)
|
|
10
10
|
|
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/telemetry.js
CHANGED
|
@@ -23,8 +23,19 @@ import { createHmac, randomUUID } from 'node:crypto'
|
|
|
23
23
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
24
24
|
import path from 'node:path'
|
|
25
25
|
|
|
26
|
-
/** 上报端点(CloudBase HTTP 访问服务)。改域名只需改这一行。
|
|
27
|
-
|
|
26
|
+
/** 上报端点(CloudBase HTTP 访问服务)。改域名只需改这一行。
|
|
27
|
+
* 可用 `DSH_REMOTE_HEARTBEAT_URL` 覆盖——存在的理由不是"让用户改"
|
|
28
|
+
* (默认行为不该需要配置),而是:① 测试必须能把它重定向掉,
|
|
29
|
+
* 否则跑一次 npm test 就会往生产统计里灌假装机(发生过);
|
|
30
|
+
* ② 作者换端点/灰度时不必为了改一行字符串发一次版。 */
|
|
31
|
+
const DEFAULT_HEARTBEAT_URL = 'https://gitbolg-d7gmnsrw46e011706-1256429518.ap-shanghai.app.tcloudbase.com/dsh-hb/heartbeat'
|
|
32
|
+
|
|
33
|
+
/** 解析实际使用的端点(env 覆盖优先,仅接受 https,避免被降级到明文)。 */
|
|
34
|
+
export function heartbeatUrl() {
|
|
35
|
+
const override = process.env.DSH_REMOTE_HEARTBEAT_URL
|
|
36
|
+
if (override && /^https:\/\//i.test(override)) return override
|
|
37
|
+
return DEFAULT_HEARTBEAT_URL
|
|
38
|
+
}
|
|
28
39
|
/** 伪名盐:服务端无法反推身份,且换盐即可切断历史关联。 */
|
|
29
40
|
const SALT = 'dsh-remote/telemetry/v1'
|
|
30
41
|
const ID_FILE = '.dsh-remote-install-id'
|
|
@@ -80,16 +91,34 @@ export function pseudonym(home) {
|
|
|
80
91
|
/** 是否到该上报了(进程内节流,避免每次工具调用都发)。 */
|
|
81
92
|
let lastSentAt = 0
|
|
82
93
|
|
|
94
|
+
/** 是否处于测试进程。
|
|
95
|
+
*
|
|
96
|
+
* ★ 这是一道**代码级**兜底,不是便利设施。事故背景:光靠 package.json 里的
|
|
97
|
+
* `--import ./test/setup-telemetry-off.mjs` 前置不够——它只覆盖 `npm test`,
|
|
98
|
+
* 任何人直接 `node --test test/upload.test.js` 就会绕过它,而 upload.test.js
|
|
99
|
+
* 会调用真实 apply(),于是**线上日活表被写进来自测试的假装机**
|
|
100
|
+
* (实测:直接跑一次,云端立刻多一行;曾出现"只用过 Windows 的机器冒出 Linux 装机")。
|
|
101
|
+
*
|
|
102
|
+
* Node 的测试运行器会在**子进程**里设 `NODE_TEST_CONTEXT`(如 child-v8),
|
|
103
|
+
* 父进程可能没有,所以同时看父进程的 `--test` 参数。判定为真时直接不发。
|
|
104
|
+
* 代价:测试里也就无法验证"真的发出去了"——那正是本函数要保证的,
|
|
105
|
+
* 真机端到端请用隔离沙箱启动真实 DSH(README 有说明)。
|
|
106
|
+
*/
|
|
107
|
+
export function inTestProcess() {
|
|
108
|
+
if (process.env.NODE_TEST_CONTEXT) return true
|
|
109
|
+
const execArgv = process.execArgv || []
|
|
110
|
+
if (execArgv.some((a) => a === '--test' || String(a).startsWith('--test-'))) return true
|
|
111
|
+
return false
|
|
112
|
+
}
|
|
113
|
+
|
|
83
114
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
* @
|
|
88
|
-
*
|
|
89
|
-
* 避开启动高峰。测试可传 0。
|
|
90
|
-
* @returns 是否真的发出了请求(仅用于测试与自检)。
|
|
115
|
+
* 真正执行发送(不含"是否处于测试进程"的守卫)。
|
|
116
|
+
*
|
|
117
|
+
* 拆出来的原因:守卫本身必须可测,而"能测"又不能变成生产代码的后门——
|
|
118
|
+
* 所以守卫只存在于 {@link sendHeartbeat} 里,本函数只被它与测试直接调用,
|
|
119
|
+
* 且**不导出给业务代码**(导出名带下划线前缀,见文件末尾)。
|
|
91
120
|
*/
|
|
92
|
-
|
|
121
|
+
async function deliver(home, version, options = {}) {
|
|
93
122
|
const now = Date.now()
|
|
94
123
|
if (now - lastSentAt < MIN_INTERVAL_MS) return false
|
|
95
124
|
|
|
@@ -98,8 +127,12 @@ export async function sendHeartbeat(home, version, options = {}) {
|
|
|
98
127
|
lastSentAt = now
|
|
99
128
|
try {
|
|
100
129
|
if (delayMs) await new Promise((r) => {
|
|
101
|
-
|
|
102
|
-
|
|
130
|
+
// ★ 这里**不能 unref**。曾经 unref 过延迟定时器,结果是:await 一个已
|
|
131
|
+
// unref 的定时器不会让事件循环保活,当进程恰好没有别的 pending 句柄时
|
|
132
|
+
// Node 会**直接退出**,心跳一次都发不出去(CI 上稳定复现:本地因为其它
|
|
133
|
+
// 测试还留着句柄而侥幸变绿)。延迟是为了让心跳成功,就必须让它把进程
|
|
134
|
+
// 留住这几秒;插件真实运行时也不可能因为多等 3s 影响用户。
|
|
135
|
+
setTimeout(r, delayMs)
|
|
103
136
|
})
|
|
104
137
|
const body = JSON.stringify({
|
|
105
138
|
idHash: pseudonym(home),
|
|
@@ -111,7 +144,7 @@ export async function sendHeartbeat(home, version, options = {}) {
|
|
|
111
144
|
const controller = new AbortController()
|
|
112
145
|
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS)
|
|
113
146
|
try {
|
|
114
|
-
await fetch(
|
|
147
|
+
await fetch(heartbeatUrl(), {
|
|
115
148
|
method: 'POST',
|
|
116
149
|
headers: { 'content-type': 'application/json' },
|
|
117
150
|
body,
|
|
@@ -129,6 +162,21 @@ export async function sendHeartbeat(home, version, options = {}) {
|
|
|
129
162
|
}
|
|
130
163
|
}
|
|
131
164
|
|
|
165
|
+
/**
|
|
166
|
+
* 发送一次心跳。**永不抛出**,也永不返回可导致分支的失败态:
|
|
167
|
+
* 调用方不需要 try/catch,心跳成功与否都不应改变任何行为。
|
|
168
|
+
* @param home - DSH_HOME 绝对路径。
|
|
169
|
+
* @param version - 运行中的插件版本(用 LOADED_VERSION,即真正在跑的代码版本)。
|
|
170
|
+
* @param options.delayMs - 发送前延迟(默认 {@link STARTUP_DELAY_MS});
|
|
171
|
+
* 避开启动高峰。测试可传 0。
|
|
172
|
+
* @returns 是否真的发出了请求(仅用于测试与自检)。
|
|
173
|
+
*/
|
|
174
|
+
export async function sendHeartbeat(home, version, options = {}) {
|
|
175
|
+
// 测试进程一律不外呼:统计数据的可信度比"测试里能验证外呼"重要得多。
|
|
176
|
+
if (inTestProcess()) return false
|
|
177
|
+
return deliver(home, version, options)
|
|
178
|
+
}
|
|
179
|
+
|
|
132
180
|
/** 清空节流与缓存(仅测试用)。 */
|
|
133
181
|
export function _resetForTest() {
|
|
134
182
|
lastSentAt = 0
|
|
@@ -137,12 +185,7 @@ export function _resetForTest() {
|
|
|
137
185
|
|
|
138
186
|
/** 心跳是否已配置(供设置页显示;不含端点本身)。 */
|
|
139
187
|
export function heartbeatEnabled() {
|
|
140
|
-
return
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/** 供自检/文档使用的端点(不涉密,可公开)。 */
|
|
144
|
-
export function heartbeatUrl() {
|
|
145
|
-
return HEARTBEAT_URL
|
|
188
|
+
return heartbeatUrl().startsWith('https://')
|
|
146
189
|
}
|
|
147
190
|
|
|
148
191
|
/** 身份文件是否已存在(自检用,不创建)。 */
|
|
@@ -159,5 +202,12 @@ export function installIdPath(home) {
|
|
|
159
202
|
return path.join(home, ID_FILE)
|
|
160
203
|
}
|
|
161
204
|
|
|
162
|
-
|
|
163
|
-
|
|
205
|
+
/**
|
|
206
|
+
* 测试专用入口:绕过"测试进程不外呼"的守卫,用来验证发送逻辑本身
|
|
207
|
+
* (字段白名单、节流、失败回滚、延迟)。
|
|
208
|
+
*
|
|
209
|
+
* 之所以必须存在:守卫让 sendHeartbeat 在测试里恒返回 false,于是发送逻辑
|
|
210
|
+
* 就完全测不到了。真正的安全来自**端点可覆盖**(测试把 URL 指向本地不可达地址)
|
|
211
|
+
* 而不是来自这个函数——它只是把守卫那道门打开给测试用。
|
|
212
|
+
*/
|
|
213
|
+
export const _deliverForTest = deliver
|
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>",
|
|
@@ -77,6 +77,6 @@
|
|
|
77
77
|
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6"
|
|
78
78
|
},
|
|
79
79
|
"scripts": {
|
|
80
|
-
"test": "node --test"
|
|
80
|
+
"test": "node --import ./test/setup-telemetry-off.mjs --test"
|
|
81
81
|
}
|
|
82
82
|
}
|