dsh-over-ssh 0.2.2 → 0.2.4
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 +82 -4
- package/agents/agent-manifest.json +7 -7
- package/agents/linux-amd64/dsh-over-ssh-agent +0 -0
- package/agents/linux-arm64/dsh-over-ssh-agent +0 -0
- package/cordis.patch.yml +10 -12
- package/lib/client.js +161 -60
- package/lib/file-references.js +136 -13
- package/lib/fs.js +2 -1
- package/lib/index.js +1245 -165
- package/lib/types/src/config.d.ts +44 -0
- package/lib/types/src/host/app/backend.d.ts +31 -0
- package/lib/types/src/host/app/routed-fs.d.ts +238 -0
- package/lib/types/src/host/app/session-admin/session-admin-contract.d.ts +17 -0
- package/lib/types/src/host/app/transport/transport-service.d.ts +66 -11
- package/lib/types/src/host/domain/capability.d.ts +6 -0
- package/lib/types/src/host/domain/roots/route-table.d.ts +101 -0
- package/lib/types/src/host/domain/session/session-admin.d.ts +8 -0
- package/lib/types/src/host/infra/backend/poll-watch.d.ts +119 -0
- package/lib/types/src/host/infra/backend/remote-backend.d.ts +297 -0
- package/lib/types/src/host/infra/cache/budget.d.ts +68 -0
- package/lib/types/src/host/infra/cache/versioned.d.ts +98 -0
- package/lib/types/src/host/infra/transport/ssh/askpass.d.ts +132 -0
- package/lib/types/src/host/infra/transport/transport.d.ts +48 -0
- package/lib/types/src/host/integration/agent/askpass-registry.d.ts +65 -0
- package/lib/types/src/host/integration/fs/remote-index.d.ts +183 -0
- package/lib/types/src/host/integration/roots/remote-home.d.ts +68 -0
- package/lib/types/src/host/integration/roots/root-registry.d.ts +148 -0
- package/lib/types/src/index.d.ts +7 -0
- package/lib/types/src/local-bridge.d.ts +4 -4
- package/lib/types/src/wiring/connections.d.ts +261 -0
- package/lib/types/src/wiring/install-memo.d.ts +31 -0
- package/package.json +1 -5
- package/lib/local-bridge-refs.js +0 -16
- package/lib/types/src/local-bridge-refs.d.ts +0 -25
package/lib/file-references.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import "@deepseek-ai/cordis";
|
|
3
3
|
|
|
4
4
|
// src/host/integration/fs/file-reference-provider.ts
|
|
5
|
-
import { readdir } from "node:fs/promises";
|
|
5
|
+
import { readdir, realpath, stat } from "node:fs/promises";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import "@deepseek-ai/cordis";
|
|
8
8
|
import FileReferenceService from "@deepseek-ai/dsh-file-reference";
|
|
@@ -47,7 +47,7 @@ var DEFAULTS = {
|
|
|
47
47
|
outputCap: 8 * 1024 * 1024
|
|
48
48
|
};
|
|
49
49
|
function rankMatches(paths, query, limit) {
|
|
50
|
-
const needle = query.trim().toLowerCase();
|
|
50
|
+
const needle = separatorsForMatching(query.trim().toLowerCase());
|
|
51
51
|
if (needle === "") {
|
|
52
52
|
const top = [];
|
|
53
53
|
const better = (left, right) => {
|
|
@@ -72,7 +72,8 @@ function rankMatches(paths, query, limit) {
|
|
|
72
72
|
}
|
|
73
73
|
const scored = [];
|
|
74
74
|
for (const path of paths) {
|
|
75
|
-
const
|
|
75
|
+
const normalized = separatorsForMatching(path);
|
|
76
|
+
const lower = normalized.toLowerCase();
|
|
76
77
|
const base = lower.slice(lower.lastIndexOf("/") + 1);
|
|
77
78
|
let score;
|
|
78
79
|
if (base.startsWith(needle)) score = 0;
|
|
@@ -87,11 +88,15 @@ function rankMatches(paths, query, limit) {
|
|
|
87
88
|
);
|
|
88
89
|
return scored.slice(0, limit).map((entry) => entry.path);
|
|
89
90
|
}
|
|
91
|
+
function separatorsForMatching(path) {
|
|
92
|
+
return path.includes("\\") && !path.includes("/") ? path.replaceAll("\\", "/") : path;
|
|
93
|
+
}
|
|
90
94
|
function depth(path) {
|
|
91
95
|
let segments = 0;
|
|
92
96
|
let inSegment = false;
|
|
93
97
|
for (let index = 0; index < path.length; index += 1) {
|
|
94
|
-
|
|
98
|
+
const code = path.charCodeAt(index);
|
|
99
|
+
if (code === 47 || code === 92) {
|
|
95
100
|
inSegment = false;
|
|
96
101
|
continue;
|
|
97
102
|
}
|
|
@@ -153,6 +158,14 @@ function makeLocalRoot(path, options = {}) {
|
|
|
153
158
|
function normalizeLocalPath2(path, options = {}) {
|
|
154
159
|
return normalizeLocalPath(path, { caseInsensitive: options.caseInsensitive ?? process.platform === "win32" });
|
|
155
160
|
}
|
|
161
|
+
function localPathContains(root, candidate, options = {}) {
|
|
162
|
+
const normalizedRoot = normalizeLocalPath2(root, options);
|
|
163
|
+
const normalizedCandidate = normalizeLocalPath2(candidate, options);
|
|
164
|
+
if (normalizedRoot === "/" || /^[a-z]:\/$/.test(normalizedRoot)) {
|
|
165
|
+
return normalizedCandidate.startsWith(normalizedRoot);
|
|
166
|
+
}
|
|
167
|
+
return normalizedCandidate === normalizedRoot || normalizedCandidate.startsWith(`${normalizedRoot}/`);
|
|
168
|
+
}
|
|
156
169
|
|
|
157
170
|
// src/host/integration/session-fork-pending.ts
|
|
158
171
|
var pending = /* @__PURE__ */ new Map();
|
|
@@ -248,6 +261,17 @@ var RemoteFileReferenceService = class extends FileReferenceService {
|
|
|
248
261
|
* (`local-backend.ts:53`), so there is no change signal to hook — only a short TTL.
|
|
249
262
|
*/
|
|
250
263
|
#localCache = /* @__PURE__ */ new Map();
|
|
264
|
+
/**
|
|
265
|
+
* 正在进行中的本地走查,按根合并。
|
|
266
|
+
*
|
|
267
|
+
* 为什么需要它:缓存只覆盖"上一次走查**已完成**"的情况。用户敲 `@mo` 时的间隔可能短于一次走查
|
|
268
|
+
* (10 000 条目约 68 ms),于是几次击键会各自发起一次全树走查——同一棵树在宿主事件循环上被重复
|
|
269
|
+
* 扫描。这里让并发的同根调用共享一次走查;查询不同不影响,因为**排序仍按每次调用自己的 query 现算**,
|
|
270
|
+
* 共享的只有与 query 无关的树枚举。
|
|
271
|
+
*/
|
|
272
|
+
#inflight = /* @__PURE__ */ new Map();
|
|
273
|
+
/** 已经告警过的来源标签;一次故障只提示一次,避免每个按键都刷一行日志。 */
|
|
274
|
+
#warned = /* @__PURE__ */ new Set();
|
|
251
275
|
constructor(ctx, options = {}) {
|
|
252
276
|
super(ctx);
|
|
253
277
|
this.#options = options;
|
|
@@ -279,20 +303,53 @@ var RemoteFileReferenceService = class extends FileReferenceService {
|
|
|
279
303
|
* without an index, or a cwd that is not an absolute host path.
|
|
280
304
|
*/
|
|
281
305
|
async list(agent, query, signal) {
|
|
306
|
+
signal.throwIfAborted();
|
|
282
307
|
const cwd = agent.session.header.cwd ?? process.cwd();
|
|
283
308
|
const registry = this.ctx.sshDevRoots;
|
|
284
309
|
if (registry === void 0) return [];
|
|
285
310
|
const identity = sessionIdentityOf(this.ctx, agent.session.header.id);
|
|
286
311
|
const gated = identity.sessionId === void 0 ? registry.routed.tryResolve(cwd) : registry.routed.tryResolve(cwd, { session: identity });
|
|
287
|
-
const addedTask = this.#
|
|
312
|
+
const addedTask = this.#degrade(
|
|
313
|
+
"\u672C\u4F1A\u8BDD\u6DFB\u52A0\u7684\u8FDC\u7AEF\u6839",
|
|
314
|
+
void 0,
|
|
315
|
+
() => this.#addedRemoteCandidates(cwd, identity, query, signal)
|
|
316
|
+
);
|
|
288
317
|
const ungated = gated.ok ? void 0 : registry.routed.tryResolve(cwd);
|
|
289
318
|
const implicit = gated.ok || ungated?.ok === true ? void 0 : sessionLocalRoot(cwd);
|
|
290
|
-
const namedTask = gated.ok ? this.#namedCandidates(gated.target.root, query, signal) : implicit === void 0 ? void 0 : this.#localCandidates(implicit.path, query, signal);
|
|
319
|
+
const namedTask = gated.ok ? this.#degrade("cwd \u547D\u540D\u7684\u6839", [], () => this.#namedCandidates(gated.target.root, query, signal)) : implicit === void 0 ? void 0 : this.#degrade("cwd \u81EA\u8EAB\u76EE\u5F55", [], () => this.#localCandidates(implicit.path, query, signal));
|
|
291
320
|
const [added, named] = await Promise.all([addedTask, namedTask]);
|
|
321
|
+
signal.throwIfAborted();
|
|
292
322
|
if (gated.ok) return this.#merge(added, named ?? []);
|
|
293
323
|
if (ungated?.ok === true) return added ?? [];
|
|
294
324
|
return this.#merge(added, named ?? []);
|
|
295
325
|
}
|
|
326
|
+
/**
|
|
327
|
+
* Run one candidate source, degrading a failure to `fallback`.
|
|
328
|
+
*
|
|
329
|
+
* 为什么不让一次失败冒到上层:`@` 补全有两个独立来源(本会话「添加目录」的远端根、cwd 命名的根)。
|
|
330
|
+
* 旧写法直接 `Promise.all`,其中**任何一个**失败(连接断、远端超时、walk 抛错)都会让整个 `list()`
|
|
331
|
+
* 拒绝——明明本地候选已经拿到,也会一起消失。取消是例外:调用方已经取消时必须继续抛出,不能把
|
|
332
|
+
* "取消"伪装成"空结果"。
|
|
333
|
+
*
|
|
334
|
+
* @param label - 日志里标识来源的短名,同一来源只告警一次。
|
|
335
|
+
* @param fallback - 失败时返回的降级值。
|
|
336
|
+
* @param task - 真正的枚举任务。
|
|
337
|
+
* @returns 任务结果,或降级值。
|
|
338
|
+
*/
|
|
339
|
+
async #degrade(label, fallback, task) {
|
|
340
|
+
try {
|
|
341
|
+
return await task();
|
|
342
|
+
} catch (error) {
|
|
343
|
+
if (isAbortError(error)) throw error;
|
|
344
|
+
if (!this.#warned.has(label)) {
|
|
345
|
+
this.#warned.add(label);
|
|
346
|
+
this.ctx.logger?.warn?.(
|
|
347
|
+
`file-references: ${label} \u53D6\u5019\u9009\u5931\u8D25\uFF0C\u672C\u6B21\u53EA\u8DF3\u8FC7\u8BE5\u6765\u6E90\uFF1A${error instanceof Error ? error.message : String(error)}`
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
return fallback;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
296
353
|
/** cwd 命名的那个根自己的候选:远端走索引,本地走有界走查。 */
|
|
297
354
|
#namedCandidates(root, query, signal) {
|
|
298
355
|
return root.location === "remote" && root.connectionId !== void 0 ? this.#remoteCandidates(root.connectionId, root.path, query, signal) : this.#localCandidates(root.path, query, signal);
|
|
@@ -359,6 +416,7 @@ var RemoteFileReferenceService = class extends FileReferenceService {
|
|
|
359
416
|
* @returns the entries, from cache when the previous walk is still fresh.
|
|
360
417
|
*/
|
|
361
418
|
async #walkCached(root, walk, signal) {
|
|
419
|
+
signal.throwIfAborted();
|
|
362
420
|
const started = this.#now();
|
|
363
421
|
const cached = this.#localCache.get(root);
|
|
364
422
|
if (cached !== void 0) {
|
|
@@ -369,10 +427,33 @@ var RemoteFileReferenceService = class extends FileReferenceService {
|
|
|
369
427
|
}
|
|
370
428
|
this.#localCache.delete(root);
|
|
371
429
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
430
|
+
let shared = this.#inflight.get(root);
|
|
431
|
+
if (shared === void 0) {
|
|
432
|
+
const controller = new AbortController();
|
|
433
|
+
const entry = { controller, waiters: 0, promise: Promise.resolve([]) };
|
|
434
|
+
const promise = (async () => {
|
|
435
|
+
const entries = await walk(root, controller.signal);
|
|
436
|
+
controller.signal.throwIfAborted();
|
|
437
|
+
this.#rememberLocal(root, entries, started);
|
|
438
|
+
return entries;
|
|
439
|
+
})();
|
|
440
|
+
entry.promise = promise;
|
|
441
|
+
void promise.catch(() => void 0).finally(() => {
|
|
442
|
+
if (this.#inflight.get(root) === entry) this.#inflight.delete(root);
|
|
443
|
+
});
|
|
444
|
+
this.#inflight.set(root, entry);
|
|
445
|
+
shared = entry;
|
|
446
|
+
}
|
|
447
|
+
shared.waiters += 1;
|
|
448
|
+
try {
|
|
449
|
+
return await raceAbort(shared.promise, signal);
|
|
450
|
+
} finally {
|
|
451
|
+
shared.waiters -= 1;
|
|
452
|
+
if (shared.waiters === 0 && this.#inflight.get(root) === shared) {
|
|
453
|
+
this.#inflight.delete(root);
|
|
454
|
+
shared.controller.abort(new DOMException("no active @ completion callers", "AbortError"));
|
|
455
|
+
}
|
|
456
|
+
}
|
|
376
457
|
}
|
|
377
458
|
/** Store one local enumeration, evicting the least recently used root past the cap. */
|
|
378
459
|
#rememberLocal(root, entries, at) {
|
|
@@ -388,6 +469,12 @@ var RemoteFileReferenceService = class extends FileReferenceService {
|
|
|
388
469
|
const maxEntries = this.#options.maxEntries ?? LOCAL_MAX_ENTRIES;
|
|
389
470
|
const maxDepth = this.#options.maxDepth ?? LOCAL_MAX_DEPTH;
|
|
390
471
|
const found = [];
|
|
472
|
+
const visited = /* @__PURE__ */ new Set();
|
|
473
|
+
const rootReal = await realpath(root).catch(() => root);
|
|
474
|
+
const caseInsensitive = process.platform === "win32";
|
|
475
|
+
const withinRoot = (candidate) => localPathContains(rootReal, candidate, { caseInsensitive });
|
|
476
|
+
visited.add(rootReal);
|
|
477
|
+
const excluded = (name) => LOCAL_EXCLUDED_DIRECTORIES.includes(caseInsensitive ? name.toLowerCase() : name);
|
|
391
478
|
let frontier = [{ path: root, depth: 0 }];
|
|
392
479
|
while (frontier.length > 0 && found.length < maxEntries) {
|
|
393
480
|
signal.throwIfAborted();
|
|
@@ -403,11 +490,27 @@ var RemoteFileReferenceService = class extends FileReferenceService {
|
|
|
403
490
|
}
|
|
404
491
|
for (const child of children) {
|
|
405
492
|
if (found.length >= maxEntries) break;
|
|
406
|
-
|
|
407
|
-
|
|
493
|
+
let isDirectory = child.isDirectory();
|
|
494
|
+
let traversable = child.isDirectory();
|
|
408
495
|
const path = join(entry.path, child.name);
|
|
496
|
+
if (!isDirectory && child.isSymbolicLink()) {
|
|
497
|
+
try {
|
|
498
|
+
const target = await stat(path);
|
|
499
|
+
isDirectory = target.isDirectory();
|
|
500
|
+
if (isDirectory) {
|
|
501
|
+
const real = await realpath(path);
|
|
502
|
+
if (visited.has(real) || !withinRoot(real)) traversable = false;
|
|
503
|
+
else {
|
|
504
|
+
visited.add(real);
|
|
505
|
+
traversable = true;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
} catch {
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (isDirectory && excluded(child.name)) continue;
|
|
409
512
|
found.push({ path, kind: isDirectory ? "directory" : "file" });
|
|
410
|
-
if (
|
|
513
|
+
if (traversable) next.push({ path, depth: entry.depth + 1 });
|
|
411
514
|
}
|
|
412
515
|
}
|
|
413
516
|
frontier = next;
|
|
@@ -415,6 +518,26 @@ var RemoteFileReferenceService = class extends FileReferenceService {
|
|
|
415
518
|
return found;
|
|
416
519
|
}
|
|
417
520
|
};
|
|
521
|
+
function raceAbort(promise, signal) {
|
|
522
|
+
if (signal.aborted) return Promise.reject(signal.reason ?? new DOMException("aborted", "AbortError"));
|
|
523
|
+
return new Promise((resolve, reject) => {
|
|
524
|
+
const onAbort = () => reject(signal.reason ?? new DOMException("aborted", "AbortError"));
|
|
525
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
526
|
+
promise.then(
|
|
527
|
+
(value) => {
|
|
528
|
+
signal.removeEventListener("abort", onAbort);
|
|
529
|
+
resolve(value);
|
|
530
|
+
},
|
|
531
|
+
(error) => {
|
|
532
|
+
signal.removeEventListener("abort", onAbort);
|
|
533
|
+
reject(error);
|
|
534
|
+
}
|
|
535
|
+
);
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
function isAbortError(error) {
|
|
539
|
+
return error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError") || typeof error === "object" && error !== null && "name" in error && (error.name === "AbortError" || error.name === "TimeoutError");
|
|
540
|
+
}
|
|
418
541
|
function sessionLocalRoot(cwd) {
|
|
419
542
|
if (cwd === "" || !isAbsoluteLocalPath(cwd)) return void 0;
|
|
420
543
|
return makeLocalRoot(cwd);
|
package/lib/fs.js
CHANGED
|
@@ -787,7 +787,8 @@ function wireGitStatus(ctx) {
|
|
|
787
787
|
if (connections === void 0 || transports === void 0) return void 0;
|
|
788
788
|
const profile = connections.get(connectionId);
|
|
789
789
|
if (profile === void 0) return void 0;
|
|
790
|
-
const
|
|
790
|
+
const lanes = ctx.get("sshDevTransportLanes");
|
|
791
|
+
const acquired = lanes === void 0 ? await transports.acquire(profile) : await lanes.acquire(profile, "compute");
|
|
791
792
|
return acquired.transport;
|
|
792
793
|
},
|
|
793
794
|
resolveRoot: (connectionId, path) => {
|