dsh-over-ssh 0.2.1 → 0.2.3
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 +42 -3
- package/agents/agent-manifest.json +4 -4
- 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 +194 -62
- package/lib/file-references.js +153 -13
- package/lib/fs.js +87 -18
- package/lib/index.js +710 -179
- package/lib/terminal-wrapper.js +4 -3
- package/lib/types/src/host/app/session-admin/session-admin-contract.d.ts +28 -2
- package/lib/types/src/host/domain/session/session-admin.d.ts +8 -0
- package/lib/types/src/host/infra/transport/ssh/ssh2-transport.d.ts +8 -0
- package/lib/types/src/host/integration/fs/fs-provider.d.ts +9 -3
- package/lib/types/src/host/integration/session/session-header.d.ts +200 -0
- package/lib/types/src/local-bridge.d.ts +4 -4
- package/lib/types/src/wiring/helpers.d.ts +1 -1
- 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,12 +158,37 @@ 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
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/host/integration/session-fork-pending.ts
|
|
171
|
+
var pending = /* @__PURE__ */ new Map();
|
|
172
|
+
function pendingForkSources(sessionId) {
|
|
173
|
+
if (sessionId === void 0 || sessionId === "") return [];
|
|
174
|
+
const source = pending.get(sessionId);
|
|
175
|
+
return source === void 0 ? [] : [source];
|
|
176
|
+
}
|
|
156
177
|
|
|
157
178
|
// src/host/integration/session-scope.ts
|
|
158
179
|
var MAX_FORK_DEPTH = 32;
|
|
159
180
|
var ancestorCache = /* @__PURE__ */ new Map();
|
|
160
181
|
var ANCESTOR_CACHE_MAX = 256;
|
|
161
182
|
function readSessionAncestors(ctx, sessionId) {
|
|
183
|
+
const pending2 = pendingForkSources(sessionId);
|
|
184
|
+
if (pending2.length > 0) {
|
|
185
|
+
const inherited = /* @__PURE__ */ new Set();
|
|
186
|
+
for (const source of pending2) {
|
|
187
|
+
inherited.add(source);
|
|
188
|
+
for (const ancestor of walkAncestors(ctx, source).ancestors) inherited.add(ancestor);
|
|
189
|
+
}
|
|
190
|
+
return [...inherited];
|
|
191
|
+
}
|
|
162
192
|
const cached = ancestorCache.get(sessionId);
|
|
163
193
|
if (cached !== void 0) {
|
|
164
194
|
ancestorCache.delete(sessionId);
|
|
@@ -231,6 +261,17 @@ var RemoteFileReferenceService = class extends FileReferenceService {
|
|
|
231
261
|
* (`local-backend.ts:53`), so there is no change signal to hook — only a short TTL.
|
|
232
262
|
*/
|
|
233
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();
|
|
234
275
|
constructor(ctx, options = {}) {
|
|
235
276
|
super(ctx);
|
|
236
277
|
this.#options = options;
|
|
@@ -262,20 +303,53 @@ var RemoteFileReferenceService = class extends FileReferenceService {
|
|
|
262
303
|
* without an index, or a cwd that is not an absolute host path.
|
|
263
304
|
*/
|
|
264
305
|
async list(agent, query, signal) {
|
|
306
|
+
signal.throwIfAborted();
|
|
265
307
|
const cwd = agent.session.header.cwd ?? process.cwd();
|
|
266
308
|
const registry = this.ctx.sshDevRoots;
|
|
267
309
|
if (registry === void 0) return [];
|
|
268
310
|
const identity = sessionIdentityOf(this.ctx, agent.session.header.id);
|
|
269
311
|
const gated = identity.sessionId === void 0 ? registry.routed.tryResolve(cwd) : registry.routed.tryResolve(cwd, { session: identity });
|
|
270
|
-
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
|
+
);
|
|
271
317
|
const ungated = gated.ok ? void 0 : registry.routed.tryResolve(cwd);
|
|
272
318
|
const implicit = gated.ok || ungated?.ok === true ? void 0 : sessionLocalRoot(cwd);
|
|
273
|
-
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));
|
|
274
320
|
const [added, named] = await Promise.all([addedTask, namedTask]);
|
|
321
|
+
signal.throwIfAborted();
|
|
275
322
|
if (gated.ok) return this.#merge(added, named ?? []);
|
|
276
323
|
if (ungated?.ok === true) return added ?? [];
|
|
277
324
|
return this.#merge(added, named ?? []);
|
|
278
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
|
+
}
|
|
279
353
|
/** cwd 命名的那个根自己的候选:远端走索引,本地走有界走查。 */
|
|
280
354
|
#namedCandidates(root, query, signal) {
|
|
281
355
|
return root.location === "remote" && root.connectionId !== void 0 ? this.#remoteCandidates(root.connectionId, root.path, query, signal) : this.#localCandidates(root.path, query, signal);
|
|
@@ -342,6 +416,7 @@ var RemoteFileReferenceService = class extends FileReferenceService {
|
|
|
342
416
|
* @returns the entries, from cache when the previous walk is still fresh.
|
|
343
417
|
*/
|
|
344
418
|
async #walkCached(root, walk, signal) {
|
|
419
|
+
signal.throwIfAborted();
|
|
345
420
|
const started = this.#now();
|
|
346
421
|
const cached = this.#localCache.get(root);
|
|
347
422
|
if (cached !== void 0) {
|
|
@@ -352,10 +427,33 @@ var RemoteFileReferenceService = class extends FileReferenceService {
|
|
|
352
427
|
}
|
|
353
428
|
this.#localCache.delete(root);
|
|
354
429
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
+
}
|
|
359
457
|
}
|
|
360
458
|
/** Store one local enumeration, evicting the least recently used root past the cap. */
|
|
361
459
|
#rememberLocal(root, entries, at) {
|
|
@@ -371,6 +469,12 @@ var RemoteFileReferenceService = class extends FileReferenceService {
|
|
|
371
469
|
const maxEntries = this.#options.maxEntries ?? LOCAL_MAX_ENTRIES;
|
|
372
470
|
const maxDepth = this.#options.maxDepth ?? LOCAL_MAX_DEPTH;
|
|
373
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);
|
|
374
478
|
let frontier = [{ path: root, depth: 0 }];
|
|
375
479
|
while (frontier.length > 0 && found.length < maxEntries) {
|
|
376
480
|
signal.throwIfAborted();
|
|
@@ -386,11 +490,27 @@ var RemoteFileReferenceService = class extends FileReferenceService {
|
|
|
386
490
|
}
|
|
387
491
|
for (const child of children) {
|
|
388
492
|
if (found.length >= maxEntries) break;
|
|
389
|
-
|
|
390
|
-
|
|
493
|
+
let isDirectory = child.isDirectory();
|
|
494
|
+
let traversable = child.isDirectory();
|
|
391
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;
|
|
392
512
|
found.push({ path, kind: isDirectory ? "directory" : "file" });
|
|
393
|
-
if (
|
|
513
|
+
if (traversable) next.push({ path, depth: entry.depth + 1 });
|
|
394
514
|
}
|
|
395
515
|
}
|
|
396
516
|
frontier = next;
|
|
@@ -398,6 +518,26 @@ var RemoteFileReferenceService = class extends FileReferenceService {
|
|
|
398
518
|
return found;
|
|
399
519
|
}
|
|
400
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
|
+
}
|
|
401
541
|
function sessionLocalRoot(cwd) {
|
|
402
542
|
if (cwd === "" || !isAbsoluteLocalPath(cwd)) return void 0;
|
|
403
543
|
return makeLocalRoot(cwd);
|
package/lib/fs.js
CHANGED
|
@@ -630,6 +630,20 @@ async function readBody(req, maxBytes) {
|
|
|
630
630
|
}
|
|
631
631
|
return Buffer.concat(chunks).toString("utf8");
|
|
632
632
|
}
|
|
633
|
+
async function readJsonObject(req, maxBytes) {
|
|
634
|
+
const text = (await readBody(req, maxBytes)).trim();
|
|
635
|
+
if (text === "") return {};
|
|
636
|
+
let parsed;
|
|
637
|
+
try {
|
|
638
|
+
parsed = JSON.parse(text);
|
|
639
|
+
} catch {
|
|
640
|
+
throw new SshDevError("ROOT_INVALID", "\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5 JSON");
|
|
641
|
+
}
|
|
642
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
643
|
+
throw new SshDevError("ROOT_INVALID", "\u8BF7\u6C42\u4F53\u9876\u5C42\u5FC5\u987B\u662F\u5BF9\u8C61");
|
|
644
|
+
}
|
|
645
|
+
return parsed;
|
|
646
|
+
}
|
|
633
647
|
function toBytes(chunk) {
|
|
634
648
|
if (typeof chunk === "string") return Buffer.from(chunk, "utf8");
|
|
635
649
|
if (chunk instanceof Uint8Array) return chunk;
|
|
@@ -650,6 +664,38 @@ function asRecord(value) {
|
|
|
650
664
|
function messageOf(error) {
|
|
651
665
|
return error instanceof Error ? error.message : String(error);
|
|
652
666
|
}
|
|
667
|
+
function reasonOf(error) {
|
|
668
|
+
const reason = asRecord(asRecord(error)?.["details"])?.["reason"];
|
|
669
|
+
return typeof reason === "string" && reason !== "" ? reason : void 0;
|
|
670
|
+
}
|
|
671
|
+
var STATUS_BY_CODE = {
|
|
672
|
+
// 输入问题:调用方改一下请求就能成功。
|
|
673
|
+
ROOT_INVALID: 400,
|
|
674
|
+
CONNECTION_INVALID: 400,
|
|
675
|
+
CONNECTION_NOT_FOUND: 400,
|
|
676
|
+
// 路径不在任何已注册的根内 / 根没有注册:客户端换一个路径或先加根就能成功,不是服务器故障。
|
|
677
|
+
// 它此前漏在表外,于是**所有**经 HTTP 暴露的根外访问都报 500(审计 2026-11-06 第 4 轮 P1)。
|
|
678
|
+
ROOT_OUTSIDE: 400,
|
|
679
|
+
// 目标不存在 / 状态冲突 / 本组合不支持:客户端可以据此换一条路。
|
|
680
|
+
TRASH_NOT_FOUND: 404,
|
|
681
|
+
TRANSFER_CONFLICT: 409,
|
|
682
|
+
SESSION_ACTIVE: 409,
|
|
683
|
+
REMOTE_UNSUPPORTED: 501,
|
|
684
|
+
// 调用方主动取消(客户端断开/关掉面板)。499 是 nginx 的 "client closed request" 约定:它不是一个错误,
|
|
685
|
+
// 只是说明"没人等这个回答了";映射成 500 会让日志里出现一堆假故障。
|
|
686
|
+
CANCELLED: 499
|
|
687
|
+
};
|
|
688
|
+
function codeOf(error) {
|
|
689
|
+
const code = asRecord(error)?.["code"];
|
|
690
|
+
return typeof code === "string" && code !== "" ? code : void 0;
|
|
691
|
+
}
|
|
692
|
+
function statusForCode(code) {
|
|
693
|
+
if (code === void 0) return 500;
|
|
694
|
+
return STATUS_BY_CODE[code] ?? 500;
|
|
695
|
+
}
|
|
696
|
+
function statusForError(error) {
|
|
697
|
+
return statusForCode(codeOf(error));
|
|
698
|
+
}
|
|
653
699
|
|
|
654
700
|
// src/host/infra/transport/git-status-route.ts
|
|
655
701
|
var GIT_STATUS_API_PATH2 = GIT_STATUS_API_PATH;
|
|
@@ -668,9 +714,9 @@ function toPublicAnswer(snapshot) {
|
|
|
668
714
|
function createGitStatusApi(deps) {
|
|
669
715
|
const handle = async (req, res) => {
|
|
670
716
|
const method = (req.method ?? "GET").toUpperCase();
|
|
671
|
-
if (method !== "POST"
|
|
672
|
-
res.setHeader("allow", "
|
|
673
|
-
send(req, res, 405, { ok: false, error:
|
|
717
|
+
if (method !== "POST") {
|
|
718
|
+
res.setHeader("allow", "POST");
|
|
719
|
+
send(req, res, 405, { ok: false, error: "git-status \u53EA\u63A5\u53D7 POST" });
|
|
674
720
|
return;
|
|
675
721
|
}
|
|
676
722
|
const verdict = trustVerdict(req);
|
|
@@ -678,13 +724,8 @@ function createGitStatusApi(deps) {
|
|
|
678
724
|
send(req, res, 403, { ok: false, error: trustMessage(verdict) });
|
|
679
725
|
return;
|
|
680
726
|
}
|
|
681
|
-
if (method !== "POST") {
|
|
682
|
-
res.setHeader("allow", "POST");
|
|
683
|
-
send(req, res, 405, { ok: false, error: "git-status \u53EA\u63A5\u53D7 POST" });
|
|
684
|
-
return;
|
|
685
|
-
}
|
|
686
727
|
try {
|
|
687
|
-
const body =
|
|
728
|
+
const body = await readJsonObject(req, ROOTS_API_MAX_BODY_BYTES);
|
|
688
729
|
const connectionId = body?.["connectionId"];
|
|
689
730
|
const path = body?.["path"];
|
|
690
731
|
if (typeof connectionId !== "string" || connectionId === "") {
|
|
@@ -708,8 +749,14 @@ function createGitStatusApi(deps) {
|
|
|
708
749
|
}
|
|
709
750
|
send(req, res, 200, toPublicAnswer(snapshot));
|
|
710
751
|
} catch (error) {
|
|
711
|
-
const
|
|
712
|
-
|
|
752
|
+
const code = codeOf(error);
|
|
753
|
+
const reason = reasonOf(error);
|
|
754
|
+
send(req, res, statusForError(error), {
|
|
755
|
+
ok: false,
|
|
756
|
+
error: messageOf(error),
|
|
757
|
+
...code === void 0 ? {} : { code },
|
|
758
|
+
...reason === void 0 ? {} : { reason }
|
|
759
|
+
});
|
|
713
760
|
}
|
|
714
761
|
};
|
|
715
762
|
return { handle };
|
|
@@ -780,6 +827,16 @@ function replaceLiteral(source, needle, replacement, all) {
|
|
|
780
827
|
|
|
781
828
|
// src/host/integration/session-scope.ts
|
|
782
829
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
830
|
+
|
|
831
|
+
// src/host/integration/session-fork-pending.ts
|
|
832
|
+
var pending = /* @__PURE__ */ new Map();
|
|
833
|
+
function pendingForkSources(sessionId) {
|
|
834
|
+
if (sessionId === void 0 || sessionId === "") return [];
|
|
835
|
+
const source = pending.get(sessionId);
|
|
836
|
+
return source === void 0 ? [] : [source];
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// src/host/integration/session-scope.ts
|
|
783
840
|
var STORE_KEY = /* @__PURE__ */ Symbol.for("dsh-over-ssh/session-scope");
|
|
784
841
|
function store() {
|
|
785
842
|
const holder = globalThis;
|
|
@@ -796,6 +853,15 @@ var MAX_FORK_DEPTH = 32;
|
|
|
796
853
|
var ancestorCache = /* @__PURE__ */ new Map();
|
|
797
854
|
var ANCESTOR_CACHE_MAX = 256;
|
|
798
855
|
function readSessionAncestors(ctx, sessionId) {
|
|
856
|
+
const pending2 = pendingForkSources(sessionId);
|
|
857
|
+
if (pending2.length > 0) {
|
|
858
|
+
const inherited = /* @__PURE__ */ new Set();
|
|
859
|
+
for (const source of pending2) {
|
|
860
|
+
inherited.add(source);
|
|
861
|
+
for (const ancestor of walkAncestors(ctx, source).ancestors) inherited.add(ancestor);
|
|
862
|
+
}
|
|
863
|
+
return [...inherited];
|
|
864
|
+
}
|
|
799
865
|
const cached = ancestorCache.get(sessionId);
|
|
800
866
|
if (cached !== void 0) {
|
|
801
867
|
ancestorCache.delete(sessionId);
|
|
@@ -857,7 +923,7 @@ function readSessionCwd(ctx, sessionId) {
|
|
|
857
923
|
}
|
|
858
924
|
return found.cwd;
|
|
859
925
|
}
|
|
860
|
-
function
|
|
926
|
+
function readSessionWorkspaces(ctx) {
|
|
861
927
|
let list;
|
|
862
928
|
try {
|
|
863
929
|
const sessions = ctx.get("sessions");
|
|
@@ -866,12 +932,14 @@ function readSessionCwds(ctx) {
|
|
|
866
932
|
return [];
|
|
867
933
|
}
|
|
868
934
|
if (!Array.isArray(list)) return [];
|
|
869
|
-
const
|
|
935
|
+
const pairs = [];
|
|
870
936
|
for (const session of list) {
|
|
937
|
+
const id = session?.header?.id;
|
|
871
938
|
const cwd = session?.header?.cwd;
|
|
872
|
-
if (typeof
|
|
939
|
+
if (typeof id !== "string" || id === "" || typeof cwd !== "string" || cwd === "") continue;
|
|
940
|
+
pairs.push({ sessionId: id, cwd });
|
|
873
941
|
}
|
|
874
|
-
return
|
|
942
|
+
return pairs;
|
|
875
943
|
}
|
|
876
944
|
var sessionCwdCache = /* @__PURE__ */ new Map();
|
|
877
945
|
var SESSION_CWD_CACHE_MAX = 256;
|
|
@@ -940,7 +1008,7 @@ var RoutedFsProvider = class extends FileSystem {
|
|
|
940
1008
|
// it"). Every method here may run with the Proxy as `this`, so the state it touches cannot be `#private`.
|
|
941
1009
|
rangeReader;
|
|
942
1010
|
rangeChunkBytes;
|
|
943
|
-
/** Reads the live sessions' working directories; see {@link RoutedFsProviderOptions.sessionWorkspaceCwds}. */
|
|
1011
|
+
/** Reads the live sessions' working directories (with ids when available); see {@link RoutedFsProviderOptions.sessionWorkspaceCwds}. */
|
|
944
1012
|
sessionWorkspaceCwds;
|
|
945
1013
|
/** Reads one session's fork lineage; see {@link RoutedFsProviderOptions.sessionAncestors}. */
|
|
946
1014
|
sessionAncestors;
|
|
@@ -955,7 +1023,7 @@ var RoutedFsProvider = class extends FileSystem {
|
|
|
955
1023
|
constructor(ctx, options = {}) {
|
|
956
1024
|
super(ctx);
|
|
957
1025
|
this.rangeReader = options.readBytesRange;
|
|
958
|
-
this.sessionWorkspaceCwds = options.sessionWorkspaceCwds ?? (() =>
|
|
1026
|
+
this.sessionWorkspaceCwds = options.sessionWorkspaceCwds ?? (() => readSessionWorkspaces(ctx));
|
|
959
1027
|
this.sessionAncestors = options.sessionAncestors ?? ((sessionId) => readSessionAncestors(ctx, sessionId));
|
|
960
1028
|
this.sessionSync = { lastSignature: "" };
|
|
961
1029
|
startHostErrorDetection();
|
|
@@ -1482,7 +1550,8 @@ async function* pagedText(reader, chunkBytes, routed, firstWindow, signal) {
|
|
|
1482
1550
|
}
|
|
1483
1551
|
function sessionSyncSignature(cwds, registry) {
|
|
1484
1552
|
const roots = registry.sessionWorkspaceRecords.map((record) => record.key).join("\0");
|
|
1485
|
-
|
|
1553
|
+
const items = cwds.map((item) => typeof item === "string" ? item : `${item.sessionId}${item.cwd}`).join("\0");
|
|
1554
|
+
return `${items}${roots}`;
|
|
1486
1555
|
}
|
|
1487
1556
|
function assertText(displayPath, bytes) {
|
|
1488
1557
|
if (bytes.subarray(0, BINARY_SNIFF_BYTES).includes(0)) {
|