dsh-remote 0.8.73 → 0.8.75

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.
@@ -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
+ }