dsh-remote 0.8.72 → 0.8.74
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/LICENSE +21 -21
- package/README.en.md +470 -389
- package/README.md +529 -462
- package/cordis.patch.yml +9 -9
- package/lib/binding.js +172 -172
- package/lib/client.js +4039 -4022
- package/lib/credential.js +263 -263
- package/lib/errors.js +141 -141
- package/lib/exec-job.js +235 -235
- package/lib/file-reference.js +457 -457
- package/lib/forwards.js +258 -258
- package/lib/host-api.js +808 -0
- package/lib/hostkey.js +108 -108
- package/lib/http-transport.js +196 -196
- package/lib/ignore.js +202 -202
- package/lib/index.js +4923 -4541
- package/lib/instructions-dynamic.js +415 -415
- package/lib/openssh.js +879 -879
- package/lib/paths.js +252 -252
- package/lib/pool.js +963 -893
- package/lib/registry.js +296 -293
- package/lib/remote-fs.js +173 -173
- package/lib/remote-sandbox.js +183 -183
- package/lib/remote-shell-child.cjs +344 -305
- package/lib/remote-shell.js +542 -518
- package/lib/routes-fs.js +245 -245
- package/lib/routes-term.js +169 -169
- package/lib/search.js +235 -235
- package/lib/sftp-surface.js +119 -119
- package/lib/sshconfig.js +779 -705
- package/lib/sync.js +275 -275
- package/lib/tasks.js +95 -95
- package/lib/telemetry.js +213 -213
- package/lib/terminal.js +205 -205
- package/lib/tui/flow.js +130 -0
- package/lib/tui/format.js +332 -0
- package/lib/tui/i18n.js +344 -0
- package/lib/tui/index.js +322 -0
- package/lib/tui/provider.js +327 -0
- package/lib/tui/scene.js +996 -0
- package/lib/update.js +472 -472
- package/lib/web-attach.js +850 -850
- package/lib/workspace-cleanup.js +337 -0
- package/package.json +100 -99
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
// dsh-remote TUI — 工作区 provider(scheme: dshremote)。
|
|
2
|
+
//
|
|
3
|
+
// 契约见 dsh-tui `dsh-adapter/workspaces.d.ts` 的 TuiWorkspaceProvider:
|
|
4
|
+
// · `list(signal)` 只读**内存缓存**(宿主强制 2s 预算,绝不在这里连网)
|
|
5
|
+
// · `resolve(uri)` / `resolvePath(path, cwd)` 走宿主 api 的 ensureRemoteWorkspace
|
|
6
|
+
// · `describe(cwd)` 同步、只读本地镜像 meta(lib/binding.js#resolveMirror),
|
|
7
|
+
// 不创建任何目录
|
|
8
|
+
//
|
|
9
|
+
// cwd 必须是本地真实存在的镜像目录:会话远程身份由 session header.cwd 落在哪个
|
|
10
|
+
// 镜像目录决定。provider 的 `uri` 表达远程身份,`cwd` 永远指向镜像。
|
|
11
|
+
|
|
12
|
+
import path from 'node:path'
|
|
13
|
+
import { resolveMirror } from '../binding.js'
|
|
14
|
+
import { buildUri, joinRemote, parseUri, targetFromWorkspace } from './format.js'
|
|
15
|
+
|
|
16
|
+
/** 每台机器默认列出的最近工作区条数(与 docs/tui.md §5 一致)。 */
|
|
17
|
+
const DEFAULT_RECENT_LIMIT = 5
|
|
18
|
+
/** tuiProviderRecentLimit 的合法上限。 */
|
|
19
|
+
const MAX_RECENT_LIMIT = 20
|
|
20
|
+
|
|
21
|
+
/** 把 1..20 的限制值夹紧;非法值回落默认 5。 */
|
|
22
|
+
function recentLimit(config) {
|
|
23
|
+
if (!config || config.tuiProviderListRecent === false) return 0
|
|
24
|
+
const raw = Number(config.tuiProviderRecentLimit)
|
|
25
|
+
if (!Number.isFinite(raw) || raw <= 0) return DEFAULT_RECENT_LIMIT
|
|
26
|
+
return Math.min(MAX_RECENT_LIMIT, Math.max(1, Math.floor(raw)))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 本地镜像目录的 registry 根(`$DSH_HOME/remote-workspaces`)。
|
|
30
|
+
*
|
|
31
|
+
* 布局由 lib/index.js 的 mirrorRootFor/mirrorDirFor 固定为
|
|
32
|
+
* `<root>/<host>-<user>-<port>/<basename>`,所以从任一镜像目录上溯两级即可得到
|
|
33
|
+
* 根;`describe` 是同步方法,不能为了 root 去 await 网络。 */
|
|
34
|
+
function rootFromMirrorDir(localMirror) {
|
|
35
|
+
if (!localMirror) return ''
|
|
36
|
+
return path.dirname(path.dirname(path.resolve(String(localMirror))))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 从 cwd 路径本身认 root(layout 固定,字符串判断是纯同步的兜底)。 */
|
|
40
|
+
function rootFromCwd(cwd) {
|
|
41
|
+
const resolved = path.resolve(String(cwd || ''))
|
|
42
|
+
const marker = `${path.sep}remote-workspaces${path.sep}`
|
|
43
|
+
const index = resolved.indexOf(marker)
|
|
44
|
+
if (index < 0) return ''
|
|
45
|
+
return resolved.slice(0, index + marker.length - 1)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 创建远程工作区 provider。
|
|
50
|
+
*
|
|
51
|
+
* @param {object} deps
|
|
52
|
+
* @param {object} deps.api - 宿主进程内 API(docs/tui.md §3,apiVersion 1)。
|
|
53
|
+
* @param {object} [deps.config] - 行 config(tuiProviderListRecent / tuiProviderRecentLimit)。
|
|
54
|
+
* @param {object} [deps.logger] - cordis logger(只用于 warn,失败一律降级)。
|
|
55
|
+
* @param {string} [deps.root] - 镜像 registry 根;省略时从 listRemoteProjects 结果推导。
|
|
56
|
+
* @returns {object} TuiWorkspaceProvider(附加 `refresh()`,供 installTui 预热/订阅刷新)。
|
|
57
|
+
*/
|
|
58
|
+
export function createWorkspaceProvider({ api, config, logger, root } = {}) {
|
|
59
|
+
const state = {
|
|
60
|
+
targets: [],
|
|
61
|
+
machines: [],
|
|
62
|
+
currentId: null,
|
|
63
|
+
projects: [],
|
|
64
|
+
root: typeof root === 'string' && root ? String(root) : '',
|
|
65
|
+
ready: false,
|
|
66
|
+
}
|
|
67
|
+
let inflight = null
|
|
68
|
+
|
|
69
|
+
/** 只记日志、绝不抛错:provider 的失败不能让宿主选择器崩。 */
|
|
70
|
+
const warn = (message, error) => {
|
|
71
|
+
try {
|
|
72
|
+
if (!logger || typeof logger.warn !== 'function') return
|
|
73
|
+
const detail = error && (error.message || error)
|
|
74
|
+
logger.warn(`[dsh-remote] tui workspace provider: ${message}${detail ? ': ' + detail : ''}`)
|
|
75
|
+
} catch { /* logging is best-effort */ }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 当前镜像根:显式 deps.root > 项目缓存 > cwd 字符串兜底。 */
|
|
79
|
+
const mirrorRoot = (cwd) => {
|
|
80
|
+
if (state.root) return state.root
|
|
81
|
+
const project = state.projects.find((entry) => entry && typeof entry.dir === 'string' && entry.dir)
|
|
82
|
+
if (project) {
|
|
83
|
+
state.root = rootFromMirrorDir(project.dir)
|
|
84
|
+
if (state.root) return state.root
|
|
85
|
+
}
|
|
86
|
+
if (cwd) {
|
|
87
|
+
const derived = rootFromCwd(cwd)
|
|
88
|
+
if (derived) return derived
|
|
89
|
+
}
|
|
90
|
+
return ''
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** 按 id / 名称 / host(含 ssh-config 别名)软匹配机器。 */
|
|
94
|
+
const findMachine = (token) => {
|
|
95
|
+
const needle = String(token === undefined || token === null ? '' : token).trim().toLowerCase()
|
|
96
|
+
if (!needle) return undefined
|
|
97
|
+
return state.machines.find((m) => String(m.id || '') === String(token))
|
|
98
|
+
|| state.machines.find((m) => String(m.name || '').toLowerCase() === needle)
|
|
99
|
+
|| state.machines.find((m) => String(m.host || '').toLowerCase() === needle)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** RemoteProject(listRemoteProjects 的行)→ 保存的机器。 */
|
|
103
|
+
const machineForProject = (project) => {
|
|
104
|
+
if (!project) return undefined
|
|
105
|
+
if (project.machineId) {
|
|
106
|
+
const direct = state.machines.find((m) => String(m.id || '') === String(project.machineId))
|
|
107
|
+
if (direct) return direct
|
|
108
|
+
}
|
|
109
|
+
const alias = String(project.alias || '')
|
|
110
|
+
if (alias) {
|
|
111
|
+
const byAlias = state.machines.find((m) => m.useSshConfig && String(m.host || '') === alias)
|
|
112
|
+
if (byAlias) return byAlias
|
|
113
|
+
}
|
|
114
|
+
const host = String(project.host || '')
|
|
115
|
+
const port = Number(project.port) || 22
|
|
116
|
+
const user = String(project.username || '')
|
|
117
|
+
return state.machines.find((m) => {
|
|
118
|
+
if (m.useSshConfig) return false
|
|
119
|
+
if (String(m.host || '') !== host) return false
|
|
120
|
+
if ((Number(m.port) || 22) !== port) return false
|
|
121
|
+
return !user || String(m.username || '') === user
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** resolveMirror 的 machine 段(meta)→ 保存的机器。 */
|
|
126
|
+
const machineForMirror = (mirrorMachine) => {
|
|
127
|
+
if (!mirrorMachine) return undefined
|
|
128
|
+
const alias = String(mirrorMachine.alias || '')
|
|
129
|
+
if (alias) {
|
|
130
|
+
const byAlias = state.machines.find((m) => m.useSshConfig && String(m.host || '') === alias)
|
|
131
|
+
if (byAlias) return byAlias
|
|
132
|
+
}
|
|
133
|
+
const host = String(mirrorMachine.host || '')
|
|
134
|
+
const port = Number(mirrorMachine.port) || 22
|
|
135
|
+
const user = String(mirrorMachine.username || '')
|
|
136
|
+
return state.machines.find((m) => {
|
|
137
|
+
if (m.useSshConfig) return false
|
|
138
|
+
if (String(m.host || '') !== host) return false
|
|
139
|
+
if ((Number(m.port) || 22) !== port) return false
|
|
140
|
+
return !user || String(m.username || '') === user
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** 把一个 target 放进缓存(uri 去重,先到先得)。 */
|
|
145
|
+
const remember = (map, target) => {
|
|
146
|
+
if (!target || !target.uri || !target.cwd) return
|
|
147
|
+
if (map.has(target.uri)) return
|
|
148
|
+
map.set(target.uri, target)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** 把 resolve 出来的新 target 并进缓存,供后续 list() 使用。 */
|
|
152
|
+
const rememberInState = (target) => {
|
|
153
|
+
if (!target || !target.uri || !target.cwd) return
|
|
154
|
+
if (state.targets.some((entry) => entry.uri === target.uri)) return
|
|
155
|
+
state.targets = [...state.targets, target]
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** 调宿主 ensureRemoteWorkspace 并转成 target;失败返回 undefined(不抛)。
|
|
159
|
+
*
|
|
160
|
+
* `register` 控制是否写工作区台账:刷新(picker 的 list)必须为 false ——
|
|
161
|
+
* 仅仅打开选择器就把每台机器的最近工作区登记进用户的全局台账,是没有点过
|
|
162
|
+
* 「打开」的用户能看到的副作用;真正选中(resolve/resolvePath)才登记。 */
|
|
163
|
+
const ensureTarget = async (machine, remotePath, register = true) => {
|
|
164
|
+
if (!machine || !remotePath) return undefined
|
|
165
|
+
try {
|
|
166
|
+
const workspace = await api.ensureRemoteWorkspace({
|
|
167
|
+
machineId: machine.id,
|
|
168
|
+
remotePath,
|
|
169
|
+
register,
|
|
170
|
+
})
|
|
171
|
+
if (!workspace || !workspace.localMirror) return undefined
|
|
172
|
+
return targetFromWorkspace({
|
|
173
|
+
machine,
|
|
174
|
+
localMirror: workspace.localMirror,
|
|
175
|
+
remotePath: workspace.remotePath || remotePath,
|
|
176
|
+
title: workspace.title,
|
|
177
|
+
})
|
|
178
|
+
} catch (error) {
|
|
179
|
+
warn(`ensureRemoteWorkspace failed for ${machine.id || machine.host}:${remotePath}`, error)
|
|
180
|
+
return undefined
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** 后台刷新缓存:机器列表 + 已存在镜像 +(可选)每机最近 N 个工作区。 */
|
|
185
|
+
const refresh = () => {
|
|
186
|
+
if (inflight) return inflight
|
|
187
|
+
inflight = (async () => {
|
|
188
|
+
try {
|
|
189
|
+
const [machineResult, projectResult] = await Promise.all([
|
|
190
|
+
Promise.resolve().then(() => api.listMachines()),
|
|
191
|
+
Promise.resolve().then(() => api.listRemoteProjects()),
|
|
192
|
+
])
|
|
193
|
+
const machines = Array.isArray(machineResult && machineResult.machines)
|
|
194
|
+
? machineResult.machines.filter((m) => m && m.id)
|
|
195
|
+
: []
|
|
196
|
+
const projects = Array.isArray(projectResult) ? projectResult.filter((p) => p && p.dir) : []
|
|
197
|
+
const currentId = machineResult && machineResult.currentId !== undefined ? machineResult.currentId : null
|
|
198
|
+
|
|
199
|
+
// 先把本轮的数据装进 state,使下面的机器匹配看到最新列表。
|
|
200
|
+
state.machines = machines
|
|
201
|
+
state.currentId = currentId
|
|
202
|
+
state.projects = projects
|
|
203
|
+
if (!state.root && projects.length) {
|
|
204
|
+
const derived = rootFromMirrorDir(projects[0].dir)
|
|
205
|
+
if (derived) state.root = derived
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const targets = new Map()
|
|
209
|
+
// 1) 已存在的镜像(listRemoteProjects 只读本地文件系统,不连网)。
|
|
210
|
+
for (const project of projects) {
|
|
211
|
+
const machine = machineForProject(project)
|
|
212
|
+
if (!machine) continue
|
|
213
|
+
remember(targets, targetFromWorkspace({
|
|
214
|
+
machine,
|
|
215
|
+
localMirror: project.dir,
|
|
216
|
+
remotePath: project.remotePath,
|
|
217
|
+
}))
|
|
218
|
+
}
|
|
219
|
+
// 2) 当前机的已记录工作区(只备镜像,不登记台账——见 ensureTarget)。
|
|
220
|
+
const current = machines.find((m) => String(m.id) === String(currentId))
|
|
221
|
+
if (current && current.workspace) {
|
|
222
|
+
const target = await ensureTarget(current, current.workspace, false)
|
|
223
|
+
remember(targets, target)
|
|
224
|
+
}
|
|
225
|
+
// 3) 每台机器最近的 N 个工作区(可在 config 里关掉;同样不登记台账)。
|
|
226
|
+
const limit = recentLimit(config)
|
|
227
|
+
if (limit > 0) {
|
|
228
|
+
for (const machine of machines) {
|
|
229
|
+
const recent = Array.isArray(machine.recentWorkspaces) ? machine.recentWorkspaces : []
|
|
230
|
+
for (const remotePath of recent.slice(0, limit)) {
|
|
231
|
+
if (!remotePath) continue
|
|
232
|
+
if (targets.has(buildUri(machine.id, String(remotePath)))) continue
|
|
233
|
+
const target = await ensureTarget(machine, String(remotePath), false)
|
|
234
|
+
remember(targets, target)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
state.targets = [...targets.values()]
|
|
240
|
+
state.ready = true
|
|
241
|
+
} catch (error) {
|
|
242
|
+
warn('refresh failed', error)
|
|
243
|
+
} finally {
|
|
244
|
+
inflight = null
|
|
245
|
+
}
|
|
246
|
+
})()
|
|
247
|
+
return inflight
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** 缓存副本;缓存为空时也返回 `[]`(绝不 throw,也不在 list 里等网络)。 */
|
|
251
|
+
const list = (signal) => {
|
|
252
|
+
if (signal && signal.aborted) return []
|
|
253
|
+
if (!state.ready && !inflight) void refresh()
|
|
254
|
+
return state.targets.map((target) => ({ ...target }))
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** URI → target;scheme 不对 / 机器不存在 / ensure 失败都返回 undefined。 */
|
|
258
|
+
const resolve = async (uri, signal) => {
|
|
259
|
+
if (signal && signal.aborted) return undefined
|
|
260
|
+
const parsed = parseUri(uri)
|
|
261
|
+
if (!parsed) return undefined
|
|
262
|
+
let machine = findMachine(parsed.machineId)
|
|
263
|
+
if (!machine) {
|
|
264
|
+
await refresh()
|
|
265
|
+
machine = findMachine(parsed.machineId)
|
|
266
|
+
}
|
|
267
|
+
if (!machine) return undefined
|
|
268
|
+
const target = await ensureTarget(machine, parsed.remotePath)
|
|
269
|
+
if (target) rememberInState(target)
|
|
270
|
+
return target
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** 只读同步的镜像解析:cwd 属于某个镜像 → target;否则 undefined。 */
|
|
274
|
+
const describe = (cwd) => {
|
|
275
|
+
if (!cwd) return undefined
|
|
276
|
+
const root = mirrorRoot(String(cwd))
|
|
277
|
+
if (!root) return undefined
|
|
278
|
+
let mirror
|
|
279
|
+
try {
|
|
280
|
+
mirror = resolveMirror(String(cwd), root)
|
|
281
|
+
} catch (error) {
|
|
282
|
+
warn('describe failed', error)
|
|
283
|
+
return undefined
|
|
284
|
+
}
|
|
285
|
+
if (!mirror || !mirror.mirrorDir || !mirror.machine || !mirror.machine.host) return undefined
|
|
286
|
+
const machine = machineForMirror(mirror.machine) || {
|
|
287
|
+
// 机器已被删除,但镜像还在:仍按 meta 提供只读身份(展示用),
|
|
288
|
+
// resolvePath 会因为没有保存的机器而拒绝。
|
|
289
|
+
id: mirror.machine.alias || mirror.machine.host,
|
|
290
|
+
name: '',
|
|
291
|
+
host: mirror.machine.host,
|
|
292
|
+
port: mirror.machine.port,
|
|
293
|
+
username: mirror.machine.username,
|
|
294
|
+
}
|
|
295
|
+
return targetFromWorkspace({
|
|
296
|
+
machine,
|
|
297
|
+
localMirror: mirror.mirrorDir,
|
|
298
|
+
remotePath: mirror.remotePath,
|
|
299
|
+
})
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** cwd 已属于本 provider 的镜像时,把 path 解析成该镜像的远程路径。 */
|
|
303
|
+
const resolvePath = async (targetPath, cwd, signal) => {
|
|
304
|
+
if (signal && signal.aborted) return undefined
|
|
305
|
+
const described = describe(cwd)
|
|
306
|
+
if (!described) return undefined
|
|
307
|
+
const root = mirrorRoot(String(cwd))
|
|
308
|
+
const mirror = resolveMirror(String(cwd), root)
|
|
309
|
+
if (!mirror || !mirror.machine) return undefined
|
|
310
|
+
const machine = machineForMirror(mirror.machine)
|
|
311
|
+
if (!machine) return undefined
|
|
312
|
+
const remotePath = joinRemote(mirror.remotePath, targetPath)
|
|
313
|
+
const target = await ensureTarget(machine, remotePath)
|
|
314
|
+
if (target) rememberInState(target)
|
|
315
|
+
return target
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
schemes: ['dshremote'],
|
|
320
|
+
list,
|
|
321
|
+
resolve,
|
|
322
|
+
resolvePath,
|
|
323
|
+
describe,
|
|
324
|
+
/** 后台预热 / 订阅刷新入口(不是 TuiWorkspaceProvider 的方法,宿主忽略)。 */
|
|
325
|
+
refresh,
|
|
326
|
+
}
|
|
327
|
+
}
|