dsh-remote 0.7.4 → 0.7.5
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 +63 -1
- package/lib/index.js +225 -1
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -85,6 +85,46 @@ window.__ModuleLoader__.load({
|
|
|
85
85
|
const refresh = () => api('GET', '/dsh-remote/machines').then((r) => { setMachines(r.machines || []); setCurrentId(r.currentId || '') })
|
|
86
86
|
React.useEffect(() => { refresh() }, [])
|
|
87
87
|
|
|
88
|
+
// ── update state ────────────────────────────────────────────────────────
|
|
89
|
+
const [upd, setUpd] = React.useState(null) // { current, latest, updateAvailable, updateMode, updatedMarker }
|
|
90
|
+
const [updBusy, setUpdBusy] = React.useState(false)
|
|
91
|
+
const [updMsg, setUpdMsg] = React.useState('')
|
|
92
|
+
const [updMode, setUpdMode] = React.useState('manual')
|
|
93
|
+
const checkUpdate = (quiet) => {
|
|
94
|
+
if (updBusy) return
|
|
95
|
+
setUpdBusy(true); setUpdMsg('')
|
|
96
|
+
api('GET', '/dsh-remote/update-check')
|
|
97
|
+
.then((r) => {
|
|
98
|
+
if (!r || !r.ok) { setUpdMsg((r && r.error) || '检查更新失败'); return }
|
|
99
|
+
setUpd(r)
|
|
100
|
+
if (r.updateMode) setUpdMode(r.updateMode)
|
|
101
|
+
if (!quiet) setUpdMsg(r.updateAvailable ? '发现新版本 ' + r.latest : '已是最新版本(v' + r.current + ')')
|
|
102
|
+
})
|
|
103
|
+
.catch((e) => setUpdMsg(String((e && e.message) || e)))
|
|
104
|
+
.finally(() => setUpdBusy(false))
|
|
105
|
+
}
|
|
106
|
+
const applyUpdateNow = () => {
|
|
107
|
+
if (!upd || !upd.latest) return
|
|
108
|
+
if (!window.confirm('将更新 dsh-remote 到 v' + upd.latest + ',替换本机插件文件。更新后需重启 Harness 生效。继续?')) return
|
|
109
|
+
setUpdBusy(true); setUpdMsg('')
|
|
110
|
+
api('POST', '/dsh-remote/update-apply', { version: upd.latest })
|
|
111
|
+
.then((r) => {
|
|
112
|
+
if (r && r.ok) setUpdMsg('✅ 已更新到 v' + r.to + '(' + r.from + ' → ' + r.to + ')。重启 Harness 生效。')
|
|
113
|
+
else setUpdMsg((r && r.error) || '更新失败')
|
|
114
|
+
checkUpdate(true)
|
|
115
|
+
})
|
|
116
|
+
.catch((e) => setUpdMsg('更新失败: ' + String((e && e.message) || e)))
|
|
117
|
+
.finally(() => setUpdBusy(false))
|
|
118
|
+
}
|
|
119
|
+
// 更新模式持久化:auto/manual/off 通过 /dsh-remote/update-check 返回当前值;
|
|
120
|
+
// 修改模式这里仅本地提示(host 侧 config 由 profile 配置)。为简单可靠,
|
|
121
|
+
// 模式切换写入 host 配置目录下的 override 文件,host 加载时读取。
|
|
122
|
+
const saveUpdateMode = (mode) => {
|
|
123
|
+
setUpdMode(mode); setUpdMsg('更新模式已设为「' + (mode === 'auto' ? '自动' : mode === 'off' ? '关闭' : '手动') + '」')
|
|
124
|
+
api('POST', '/dsh-remote/update-mode', { mode }).catch((e) => setUpdMsg('模式保存失败: ' + String((e && e.message) || e)))
|
|
125
|
+
}
|
|
126
|
+
React.useEffect(() => { checkUpdate(true) }, [])
|
|
127
|
+
|
|
88
128
|
const setF = (k) => (ev) => setForm({ ...form, [k]: ev.target.value })
|
|
89
129
|
const startEdit = (m) => setForm({ name: m.name, host: m.host, port: String(m.port || 22), username: m.username || 'root', password: '', privateKeyPath: m.privateKeyPath || '', id: m.id })
|
|
90
130
|
const save = (action) => {
|
|
@@ -140,12 +180,34 @@ window.__ModuleLoader__.load({
|
|
|
140
180
|
),
|
|
141
181
|
),
|
|
142
182
|
err ? React.createElement('div', { style: { color: T.danger, fontSize: 13 } }, err) : null,
|
|
183
|
+
React.createElement('div', { style: boxS },
|
|
184
|
+
React.createElement('div', { style: { marginBottom: 6, fontSize: 13, fontWeight: 600 } }, '更新'),
|
|
185
|
+
React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap', fontSize: 12 } },
|
|
186
|
+
upd
|
|
187
|
+
? React.createElement('span', { style: { opacity: 0.85 } }, '当前 v' + upd.current + (upd.updateAvailable ? React.createElement('b', { style: { color: T.ok, marginLeft: 6 } }, '→ 新版本 v' + upd.latest) : null))
|
|
188
|
+
: React.createElement('span', { style: { opacity: 0.6 } }, '版本信息加载中…'),
|
|
189
|
+
React.createElement('button', { style: { ...buttonS, padding: '2px 10px' }, onClick: () => checkUpdate(false), disabled: updBusy }, updBusy ? '检查中…' : '检查更新'),
|
|
190
|
+
upd && upd.updateAvailable
|
|
191
|
+
? React.createElement('button', { style: { ...buttonS, padding: '2px 10px', fontWeight: 700, color: T.ok }, onClick: applyUpdateNow, disabled: updBusy }, '立即更新')
|
|
192
|
+
: null,
|
|
193
|
+
),
|
|
194
|
+
React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', marginTop: 6, fontSize: 12 } },
|
|
195
|
+
React.createElement('span', { style: { opacity: 0.8 } }, '更新模式:'),
|
|
196
|
+
['manual', 'auto', 'off'].map((m) =>
|
|
197
|
+
React.createElement('button', {
|
|
198
|
+
key: m, style: { ...buttonS, padding: '2px 10px', fontWeight: updMode === m ? 700 : 400, color: updMode === m ? T.ok : 'inherit' },
|
|
199
|
+
onClick: () => saveUpdateMode(m), disabled: updBusy,
|
|
200
|
+
}, m === 'manual' ? '手动' : m === 'auto' ? '自动' : '关闭'),
|
|
201
|
+
),
|
|
202
|
+
),
|
|
203
|
+
updMsg ? React.createElement('div', { style: { marginTop: 6, fontSize: 12, color: updMsg.startsWith('✅') || updMsg.startsWith('已是最新') ? T.ok : 'inherit', opacity: 0.9 } }, updMsg) : null,
|
|
204
|
+
),
|
|
143
205
|
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 } },
|
|
144
206
|
React.createElement('span', {}, '觉得好用?'),
|
|
145
207
|
React.createElement('a', { href: 'https://github.com/flymysql/dsh-remote', target: '_blank', rel: 'noopener noreferrer', style: { color: 'inherit' } }, '⭐ 去 GitHub 点个 Star'),
|
|
146
208
|
React.createElement('span', {}, '·'),
|
|
147
209
|
React.createElement('a', { href: 'https://github.com/flymysql/dsh-remote/issues/new', target: '_blank', rel: 'noopener noreferrer', style: { color: 'inherit' } }, '💬 反馈建议 / 提 issue'),
|
|
148
|
-
React.createElement('span', { style: { marginLeft: 'auto', opacity: 0.5 } }, 'dsh-remote v0.7.
|
|
210
|
+
React.createElement('span', { style: { marginLeft: 'auto', opacity: 0.5 } }, 'dsh-remote v0.7.5'),
|
|
149
211
|
),
|
|
150
212
|
)
|
|
151
213
|
}
|
package/lib/index.js
CHANGED
|
@@ -20,9 +20,10 @@ import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
|
20
20
|
import ssh2 from 'ssh2'
|
|
21
21
|
import { createHash } from 'node:crypto'
|
|
22
22
|
import { execFile } from 'node:child_process'
|
|
23
|
-
import { readFileSync, mkdirSync, writeFileSync, existsSync, readdirSync, statSync, renameSync, copyFileSync, utimesSync } from 'node:fs'
|
|
23
|
+
import { readFileSync, mkdirSync, writeFileSync, existsSync, readdirSync, statSync, renameSync, copyFileSync, utimesSync, rmSync } from 'node:fs'
|
|
24
24
|
import { homedir } from 'node:os'
|
|
25
25
|
import path from 'node:path'
|
|
26
|
+
import { fileURLToPath } from 'node:url'
|
|
26
27
|
|
|
27
28
|
const { Client } = ssh2
|
|
28
29
|
|
|
@@ -62,6 +63,12 @@ export const Config = z.object({
|
|
|
62
63
|
* accept-new); `verify` also rejects hosts never seen before; `off` skips
|
|
63
64
|
* verification entirely (MITM-unsafe, not recommended). */
|
|
64
65
|
hostKeyMode: z.string().default('accept-new'),
|
|
66
|
+
/** Update mode: `manual` (default) only checks when asked; `auto` checks on
|
|
67
|
+
* load and periodically, applying a newer npm release automatically;
|
|
68
|
+
* `off` disables version checks entirely. */
|
|
69
|
+
updateMode: z.enum(['manual', 'auto', 'off']).default('manual'),
|
|
70
|
+
/** How often (ms) auto mode checks the npm registry for a newer release. */
|
|
71
|
+
updateCheckIntervalMs: z.number().step(1).min(60000).default(6 * 3600 * 1000),
|
|
65
72
|
})
|
|
66
73
|
|
|
67
74
|
// ── shell / path helpers (proven in the read tooling) ───────────────────────
|
|
@@ -1462,6 +1469,169 @@ export async function apply(ctx, config) {
|
|
|
1462
1469
|
res.setHeader('Content-Type', 'application/json; charset=utf-8')
|
|
1463
1470
|
res.end(JSON.stringify(body))
|
|
1464
1471
|
}
|
|
1472
|
+
|
|
1473
|
+
// ── self-update (version check + apply) ────────────────────────────────────
|
|
1474
|
+
// dsh-remote is a zero-build plugin: the host half (lib/index.js) is loaded
|
|
1475
|
+
// straight from the installed package, so "updating" = replacing the package
|
|
1476
|
+
// files with the newer npm tarball. We never mutate node_modules directly
|
|
1477
|
+
// (pnpm owns it); instead we write the fresh files into the same directory
|
|
1478
|
+
// the loader already resolved, which is the profile's node_modules/dsh-remote
|
|
1479
|
+
// (hoisted) or the .pnpm store copy — both are writable for the local user.
|
|
1480
|
+
const NPM_PACKAGE = 'dsh-remote'
|
|
1481
|
+
const REGISTRY_URL = 'https://registry.npmjs.org/' + NPM_PACKAGE + '/latest'
|
|
1482
|
+
/** Absolute directory holding this plugin's installed files (lib/ + package.json). */
|
|
1483
|
+
const selfDir = () => {
|
|
1484
|
+
try {
|
|
1485
|
+
return path.dirname(path.dirname(fileURLToPath(import.meta.url)))
|
|
1486
|
+
} catch {
|
|
1487
|
+
return path.dirname(path.dirname(process.argv[1] || ''))
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
const readVersion = () => {
|
|
1491
|
+
try {
|
|
1492
|
+
const pkg = JSON.parse(readFileSync(path.join(selfDir(), 'package.json'), 'utf8'))
|
|
1493
|
+
return String(pkg.version || '0.0.0')
|
|
1494
|
+
} catch {
|
|
1495
|
+
return '0.0.0'
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
const currentVersion = readVersion()
|
|
1499
|
+
// A mode persisted via the settings UI (/dsh-remote/update-mode) wins over
|
|
1500
|
+
// the profile-config default, so the user's choice survives restarts without
|
|
1501
|
+
// editing cordis config.
|
|
1502
|
+
const persistedMode = (() => {
|
|
1503
|
+
try {
|
|
1504
|
+
const m = readFileSync(path.join(selfDir(), 'update-mode'), 'utf8').trim()
|
|
1505
|
+
return ['manual', 'auto', 'off'].includes(m) ? m : null
|
|
1506
|
+
} catch {
|
|
1507
|
+
return null
|
|
1508
|
+
}
|
|
1509
|
+
})()
|
|
1510
|
+
if (persistedMode) config.updateMode = persistedMode
|
|
1511
|
+
/** Query the npm registry for the latest release; resolves null on any failure. */
|
|
1512
|
+
const fetchLatestVersion = async (timeoutMs = 8000) => {
|
|
1513
|
+
try {
|
|
1514
|
+
const controller = new AbortController()
|
|
1515
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
1516
|
+
try {
|
|
1517
|
+
const res = await fetch(REGISTRY_URL, { signal: controller.signal, headers: { 'accept': 'application/json' } })
|
|
1518
|
+
if (!res.ok) return null
|
|
1519
|
+
const data = await res.json()
|
|
1520
|
+
return typeof data?.version === 'string' && data.version ? data.version : null
|
|
1521
|
+
} finally {
|
|
1522
|
+
clearTimeout(timer)
|
|
1523
|
+
}
|
|
1524
|
+
} catch {
|
|
1525
|
+
return null
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
/** Compare dotted versions; returns true when a > b. */
|
|
1529
|
+
const gtVersion = (a, b) => {
|
|
1530
|
+
const pa = String(a || '').split('.').map((n) => parseInt(n, 10) || 0)
|
|
1531
|
+
const pb = String(b || '').split('.').map((n) => parseInt(n, 10) || 0)
|
|
1532
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
1533
|
+
const da = pa[i] || 0
|
|
1534
|
+
const db = pb[i] || 0
|
|
1535
|
+
if (da !== db) return da > db
|
|
1536
|
+
}
|
|
1537
|
+
return false
|
|
1538
|
+
}
|
|
1539
|
+
/**
|
|
1540
|
+
* Download the given version's tarball and atomically replace the installed
|
|
1541
|
+
* package files (lib/*.js, cordis.patch.yml, package.json). A stale marker
|
|
1542
|
+
* file (.dsh-remote-updated) is written so the client can tell the user to
|
|
1543
|
+
* reload; the loader picks the new host half up on the next boot.
|
|
1544
|
+
*/
|
|
1545
|
+
const applyUpdate = async (targetVersion) => {
|
|
1546
|
+
const tarballUrl = `https://registry.npmjs.org/${NPM_PACKAGE}/-/${NPM_PACKAGE}-${targetVersion}.tgz`
|
|
1547
|
+
const dir = selfDir()
|
|
1548
|
+
const tmpRoot = path.join(dir, `.dsh-remote-update-${Date.now()}`)
|
|
1549
|
+
const tmpPkg = path.join(tmpRoot, 'package')
|
|
1550
|
+
try {
|
|
1551
|
+
const controller = new AbortController()
|
|
1552
|
+
const timer = setTimeout(() => controller.abort(), 60000)
|
|
1553
|
+
let res
|
|
1554
|
+
try {
|
|
1555
|
+
res = await fetch(tarballUrl, { signal: controller.signal })
|
|
1556
|
+
} finally {
|
|
1557
|
+
clearTimeout(timer)
|
|
1558
|
+
}
|
|
1559
|
+
if (!res.ok) throw new Error(`download failed (HTTP ${res.status})`)
|
|
1560
|
+
const buf = Buffer.from(await res.arrayBuffer())
|
|
1561
|
+
// npm tarballs are gzipped tar; extract the package/ directory.
|
|
1562
|
+
mkdirSync(tmpRoot, { recursive: true })
|
|
1563
|
+
// Minimal tar.gz extractor: we only need package/{lib,package.json,cordis.patch.yml}.
|
|
1564
|
+
// Use node's zlib gunzip + a tiny tar parser for the regular files we ship.
|
|
1565
|
+
const { gunzipSync } = await import('node:zlib')
|
|
1566
|
+
const tar = gunzipSync(buf)
|
|
1567
|
+
const files = parseTar(tar)
|
|
1568
|
+
let extracted = 0
|
|
1569
|
+
for (const f of files) {
|
|
1570
|
+
const rel = f.name.replace(/^package\//, '')
|
|
1571
|
+
if (!rel || rel === 'package/' || rel.endsWith('/')) continue
|
|
1572
|
+
if (rel !== 'package.json' && rel !== 'cordis.patch.yml' && !rel.startsWith('lib/')) continue
|
|
1573
|
+
const dest = path.join(tmpPkg, rel)
|
|
1574
|
+
mkdirSync(path.dirname(dest), { recursive: true })
|
|
1575
|
+
writeFileSync(dest, f.data)
|
|
1576
|
+
extracted++
|
|
1577
|
+
}
|
|
1578
|
+
if (extracted === 0) throw new Error('tarball contained no package files')
|
|
1579
|
+
// Verify the downloaded package.json matches the requested version.
|
|
1580
|
+
const newPkg = JSON.parse(readFileSync(path.join(tmpPkg, 'package.json'), 'utf8'))
|
|
1581
|
+
if (newPkg.version !== targetVersion) throw new Error(`tarball version mismatch: ${newPkg.version}`)
|
|
1582
|
+
// Atomically swap: copy fresh files over the installed ones.
|
|
1583
|
+
const installLib = path.join(dir, 'lib')
|
|
1584
|
+
const tmpLib = path.join(tmpPkg, 'lib')
|
|
1585
|
+
for (const name of ['index.js', 'client.js']) {
|
|
1586
|
+
const src = path.join(tmpLib, name)
|
|
1587
|
+
if (existsSync(src)) copyFileSync(src, path.join(installLib, name))
|
|
1588
|
+
}
|
|
1589
|
+
for (const rel of ['package.json', 'cordis.patch.yml']) {
|
|
1590
|
+
const src = path.join(tmpPkg, rel)
|
|
1591
|
+
if (existsSync(src)) copyFileSync(src, path.join(dir, rel))
|
|
1592
|
+
}
|
|
1593
|
+
writeFileSync(path.join(dir, '.dsh-remote-updated'), String(targetVersion))
|
|
1594
|
+
return { ok: true, from: currentVersion, to: targetVersion }
|
|
1595
|
+
} catch (err) {
|
|
1596
|
+
throw new Error('update failed: ' + ((err && err.message) || err))
|
|
1597
|
+
} finally {
|
|
1598
|
+
try { rmSync(tmpRoot, { recursive: true, force: true }) } catch {}
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
/** Minimal POSIX ustar parser (regular files only) for npm tarballs. */
|
|
1602
|
+
function parseTar(buf) {
|
|
1603
|
+
const files = []
|
|
1604
|
+
let off = 0
|
|
1605
|
+
while (off + 512 <= buf.length) {
|
|
1606
|
+
const header = buf.subarray(off, off + 512)
|
|
1607
|
+
const name = header.subarray(0, 100).toString('utf8').replace(/\0.*$/, '')
|
|
1608
|
+
if (!name) break
|
|
1609
|
+
const sizeStr = header.subarray(124, 136).toString('utf8').replace(/\0.*$/, '').trim()
|
|
1610
|
+
const size = parseInt(sizeStr, 8) || 0
|
|
1611
|
+
const type = String.fromCharCode(header[156] || 48)
|
|
1612
|
+
off += 512
|
|
1613
|
+
const data = type === '48' || type === '0' ? Buffer.from(buf.subarray(off, off + size)) : null
|
|
1614
|
+
if (data) files.push({ name, data })
|
|
1615
|
+
off += Math.ceil(size / 512) * 512
|
|
1616
|
+
}
|
|
1617
|
+
return files
|
|
1618
|
+
}
|
|
1619
|
+
// Auto mode: check once on load and on an interval; apply silently when a
|
|
1620
|
+
// newer version exists. Failures are swallowed (never break the plugin).
|
|
1621
|
+
let updateTimer = null
|
|
1622
|
+
if (config.updateMode === 'auto') {
|
|
1623
|
+
const checkAndApply = async () => {
|
|
1624
|
+
const latest = await fetchLatestVersion()
|
|
1625
|
+
if (latest && gtVersion(latest, currentVersion)) {
|
|
1626
|
+
try { await applyUpdate(latest) } catch {}
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
void checkAndApply()
|
|
1630
|
+
updateTimer = setInterval(checkAndApply, Math.max(config.updateCheckIntervalMs, 60000))
|
|
1631
|
+
if (typeof updateTimer.unref === 'function') updateTimer.unref()
|
|
1632
|
+
ctx.effect(() => () => { if (updateTimer) clearInterval(updateTimer) }, 'dsh-remote.update-timer')
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1465
1635
|
// Cap request bodies (1 MiB) so a malformed client cannot force us to buffer
|
|
1466
1636
|
// unbounded memory on a local-only route.
|
|
1467
1637
|
const MAX_BODY_BYTES = 1024 * 1024
|
|
@@ -1804,6 +1974,60 @@ export async function apply(ctx, config) {
|
|
|
1804
1974
|
return sendJson(res, 200, { ok: true, ...status() })
|
|
1805
1975
|
},
|
|
1806
1976
|
},
|
|
1977
|
+
{
|
|
1978
|
+
kind: 'exact',
|
|
1979
|
+
path: '/dsh-remote/update-check',
|
|
1980
|
+
handler: async (req, res) => {
|
|
1981
|
+
if (req.method !== 'GET') return sendJson(res, 405, { ok: false, error: 'method not allowed' })
|
|
1982
|
+
const latest = await fetchLatestVersion()
|
|
1983
|
+
if (latest === null) return sendJson(res, 200, { ok: false, current: currentVersion, error: '无法连接 npm registry' })
|
|
1984
|
+
const updateAvailable = gtVersion(latest, currentVersion)
|
|
1985
|
+
const updatedMarker = existsSync(path.join(selfDir(), '.dsh-remote-updated'))
|
|
1986
|
+
return sendJson(res, 200, {
|
|
1987
|
+
ok: true,
|
|
1988
|
+
current: currentVersion,
|
|
1989
|
+
latest,
|
|
1990
|
+
updateAvailable,
|
|
1991
|
+
updateMode: config.updateMode,
|
|
1992
|
+
updatedMarker,
|
|
1993
|
+
})
|
|
1994
|
+
},
|
|
1995
|
+
},
|
|
1996
|
+
{
|
|
1997
|
+
kind: 'exact',
|
|
1998
|
+
path: '/dsh-remote/update-mode',
|
|
1999
|
+
handler: async (req, res) => {
|
|
2000
|
+
if (req.method !== 'POST') return sendJson(res, 405, { ok: false, error: 'method not allowed' })
|
|
2001
|
+
try {
|
|
2002
|
+
const body = JSON.parse((await readBody(req)) || '{}')
|
|
2003
|
+
const mode = String(body.mode || '')
|
|
2004
|
+
if (!['manual', 'auto', 'off'].includes(mode)) return sendJson(res, 400, { ok: false, error: 'mode must be manual | auto | off' })
|
|
2005
|
+
// Persist to the plugin's install dir so it survives restarts; the
|
|
2006
|
+
// effective value is read back on next load by the host half.
|
|
2007
|
+
const dir = selfDir()
|
|
2008
|
+
try { writeFileSync(path.join(dir, 'update-mode'), mode) } catch (e) { return sendJson(res, 500, { ok: false, error: 'cannot persist mode: ' + ((e && e.message) || e) }) }
|
|
2009
|
+
return sendJson(res, 200, { ok: true, mode })
|
|
2010
|
+
} catch (err) {
|
|
2011
|
+
return sendJson(res, 500, { ok: false, error: String((err && err.message) || err) })
|
|
2012
|
+
}
|
|
2013
|
+
},
|
|
2014
|
+
},
|
|
2015
|
+
{
|
|
2016
|
+
kind: 'exact',
|
|
2017
|
+
path: '/dsh-remote/update-apply',
|
|
2018
|
+
handler: async (req, res) => {
|
|
2019
|
+
if (req.method !== 'POST') return sendJson(res, 405, { ok: false, error: 'method not allowed' })
|
|
2020
|
+
try {
|
|
2021
|
+
const body = JSON.parse((await readBody(req)) || '{}')
|
|
2022
|
+
const target = String(body.version || '')
|
|
2023
|
+
if (!target) return sendJson(res, 400, { ok: false, error: 'version is required' })
|
|
2024
|
+
const result = await applyUpdate(target)
|
|
2025
|
+
return sendJson(res, 200, { ok: true, ...result })
|
|
2026
|
+
} catch (err) {
|
|
2027
|
+
return sendJson(res, 500, { ok: false, error: String((err && err.message) || err) })
|
|
2028
|
+
}
|
|
2029
|
+
},
|
|
2030
|
+
},
|
|
1807
2031
|
]
|
|
1808
2032
|
|
|
1809
2033
|
const disposers = routes.map((r) => webServer.register(r))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-remote",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.5",
|
|
4
4
|
"description": "Remote-work assistant for DeepSeek Harness: connect SSH (password or key), pick a remote workspace, operate on it with rw_pick_workspace / rw_list_dir / rw_read_file / rw_exec / rw_sync tools, and mirror it to a real local directory (SFTP) so the DSH native workspace can adopt it.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "flymysql <flyphp@outlook.com>",
|