dsh-over-ssh 0.2.0 → 0.2.1
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 +4 -4
- package/agents/agent-manifest.json +6 -6
- package/agents/linux-amd64/dsh-over-ssh-agent +0 -0
- package/agents/linux-arm64/dsh-over-ssh-agent +0 -0
- package/lib/client.js +507 -222
- package/lib/fs.js +3 -2
- package/lib/index.js +890 -373
- package/lib/types/src/host/domain/session/session-admin.d.ts +5 -0
- package/lib/types/src/wiring/helpers.d.ts +10 -1
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -3126,7 +3126,13 @@ function createTransferService(dependencies) {
|
|
|
3126
3126
|
...request.signal === void 0 ? {} : { signal: request.signal }
|
|
3127
3127
|
})
|
|
3128
3128
|
});
|
|
3129
|
-
const
|
|
3129
|
+
const degradations = [...decision.degradations];
|
|
3130
|
+
let digest = { sha256: void 0, degradation: void 0 };
|
|
3131
|
+
const needsLocalDigest = move.writerSha256 !== void 0 || decision.resumed;
|
|
3132
|
+
if (needsLocalDigest) {
|
|
3133
|
+
digest = await lookupDigest(dependencies, localPath);
|
|
3134
|
+
if (digest.degradation !== void 0) degradations.push(digest.degradation);
|
|
3135
|
+
}
|
|
3130
3136
|
if (move.writerSha256 !== void 0 && digest.sha256 !== void 0 && !digestsMatch(move.writerSha256, digest.sha256)) {
|
|
3131
3137
|
throw transferFailure(
|
|
3132
3138
|
"verify-failed",
|
|
@@ -3134,12 +3140,15 @@ function createTransferService(dependencies) {
|
|
|
3134
3140
|
{ ...context, bytesTransferred: move.bytesTransferred, chunks: move.chunks, localSha256: digest.sha256, remoteSha256: move.writerSha256 }
|
|
3135
3141
|
);
|
|
3136
3142
|
}
|
|
3137
|
-
const degradations = [...decision.degradations, ...digest.degradation === void 0 ? [] : [digest.degradation]];
|
|
3138
3143
|
let landed = move.writerSha256;
|
|
3139
3144
|
if (decision.resumed && landed === void 0) {
|
|
3140
3145
|
const remote = await lookupRemoteDigest(dependencies, target.absolute, request.signal);
|
|
3141
3146
|
if (remote.degradation !== void 0) degradations.push(remote.degradation);
|
|
3142
3147
|
if (remote.sha256 !== void 0) {
|
|
3148
|
+
if (digest.sha256 === void 0) {
|
|
3149
|
+
digest = await lookupDigest(dependencies, localPath);
|
|
3150
|
+
if (digest.degradation !== void 0) degradations.push(digest.degradation);
|
|
3151
|
+
}
|
|
3143
3152
|
if (digest.sha256 !== void 0 && !digestsMatch(remote.sha256, digest.sha256)) {
|
|
3144
3153
|
throw transferFailure(
|
|
3145
3154
|
"verify-failed",
|
|
@@ -4182,7 +4191,7 @@ function planDelete(records, request) {
|
|
|
4182
4191
|
refusals.push({ id, reason: "unreadable" });
|
|
4183
4192
|
continue;
|
|
4184
4193
|
}
|
|
4185
|
-
if (record2.classification === "subsession") {
|
|
4194
|
+
if (record2.classification === "subsession" && !isOrphanSubsession(record2, byId)) {
|
|
4186
4195
|
deferred.push(id);
|
|
4187
4196
|
continue;
|
|
4188
4197
|
}
|
|
@@ -4203,6 +4212,11 @@ function planDelete(records, request) {
|
|
|
4203
4212
|
for (const id of ordered) bytes += byId.get(id)?.bytes ?? 0;
|
|
4204
4213
|
return { delete: ordered, refusals, bytes };
|
|
4205
4214
|
}
|
|
4215
|
+
function isOrphanSubsession(record2, byId) {
|
|
4216
|
+
if (record2.parentSessionId === void 0) return true;
|
|
4217
|
+
const parent = byId.get(record2.parentSessionId);
|
|
4218
|
+
return parent === void 0 || parent.classification === "unreadable";
|
|
4219
|
+
}
|
|
4206
4220
|
function descendantsOf(records, rootId) {
|
|
4207
4221
|
const children = /* @__PURE__ */ new Map();
|
|
4208
4222
|
for (const record2 of records) {
|
|
@@ -4391,6 +4405,11 @@ function createSerialQueue() {
|
|
|
4391
4405
|
};
|
|
4392
4406
|
}
|
|
4393
4407
|
|
|
4408
|
+
// src/shared/path-fold.ts
|
|
4409
|
+
function foldPathKey(path) {
|
|
4410
|
+
return path.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
|
|
4411
|
+
}
|
|
4412
|
+
|
|
4394
4413
|
// src/host/app/session-admin/session-admin-context.ts
|
|
4395
4414
|
function isMissing(error) {
|
|
4396
4415
|
const code = error?.code;
|
|
@@ -4406,18 +4425,18 @@ function createSessionAdminContext(deps) {
|
|
|
4406
4425
|
reason: "bin-inside-sessions-root"
|
|
4407
4426
|
});
|
|
4408
4427
|
}
|
|
4409
|
-
const
|
|
4428
|
+
const indexPath = () => joinPath(binRoot, SESSION_TRASH_INDEX_FILE);
|
|
4410
4429
|
const queue = createSerialQueue();
|
|
4411
4430
|
const serialized = (operation) => (
|
|
4412
4431
|
// Order matters: take the in-process slot **first** (the queue), then the cross-process lock. Holding a file
|
|
4413
4432
|
// lock while queued behind another in-process call would serialize on the wrong resource.
|
|
4414
|
-
queue.run(() => deps.withLock === void 0 ? operation() : deps.withLock(
|
|
4433
|
+
queue.run(() => deps.withLock === void 0 ? operation() : deps.withLock(indexPath(), operation))
|
|
4415
4434
|
);
|
|
4416
4435
|
const entryDir = (segment) => joinPath(binRoot, "entries", segment);
|
|
4417
4436
|
const readIndex = async () => {
|
|
4418
4437
|
let text8;
|
|
4419
4438
|
try {
|
|
4420
|
-
text8 = await deps.readText(
|
|
4439
|
+
text8 = await deps.readText(indexPath());
|
|
4421
4440
|
} catch (error) {
|
|
4422
4441
|
if (isMissing(error)) return [];
|
|
4423
4442
|
throw error;
|
|
@@ -4427,77 +4446,104 @@ function createSessionAdminContext(deps) {
|
|
|
4427
4446
|
const writeIndex = async (entries) => {
|
|
4428
4447
|
const text8 = serializeSessionTrashIndex(entries);
|
|
4429
4448
|
if (deps.writeTextAtomic !== void 0) {
|
|
4430
|
-
await deps.writeTextAtomic(
|
|
4449
|
+
await deps.writeTextAtomic(indexPath(), text8);
|
|
4431
4450
|
return;
|
|
4432
4451
|
}
|
|
4433
|
-
await deps.writeText(
|
|
4452
|
+
await deps.writeText(indexPath(), text8);
|
|
4453
|
+
};
|
|
4454
|
+
const INVENTORY_CONCURRENCY = 8;
|
|
4455
|
+
const mapWithLimit = async (items, limit, task) => {
|
|
4456
|
+
const results = new Array(items.length);
|
|
4457
|
+
let next = 0;
|
|
4458
|
+
const workers = Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, async () => {
|
|
4459
|
+
for (; ; ) {
|
|
4460
|
+
const index = next;
|
|
4461
|
+
next += 1;
|
|
4462
|
+
if (index >= items.length) return;
|
|
4463
|
+
results[index] = await task(items[index], index);
|
|
4464
|
+
}
|
|
4465
|
+
});
|
|
4466
|
+
await Promise.all(workers);
|
|
4467
|
+
return results;
|
|
4468
|
+
};
|
|
4469
|
+
const throwIfAborted = (signal) => {
|
|
4470
|
+
if (signal?.aborted === true) {
|
|
4471
|
+
throw new SshDevError("CANCELLED", "\u64CD\u4F5C\u5DF2\u53D6\u6D88", { reason: "aborted" });
|
|
4472
|
+
}
|
|
4434
4473
|
};
|
|
4435
4474
|
const inventory = async (options = {}) => {
|
|
4436
|
-
|
|
4475
|
+
const signal = options.signal;
|
|
4476
|
+
throwIfAborted(signal);
|
|
4437
4477
|
const projects = await deps.listDir(sessionsRoot);
|
|
4438
|
-
const
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
const projectDir = joinPath(sessionsRoot, project.name);
|
|
4478
|
+
const projectNames = projects.filter((project) => project.isDirectory).map((project) => project.name);
|
|
4479
|
+
const perProject = await mapWithLimit(projectNames, INVENTORY_CONCURRENCY, async (project) => {
|
|
4480
|
+
const projectDir = joinPath(sessionsRoot, project);
|
|
4442
4481
|
const sessions = await deps.listDir(projectDir);
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
});
|
|
4470
|
-
continue;
|
|
4471
|
-
}
|
|
4472
|
-
const title = header.title ?? deps.resolveSessionTitle?.(header);
|
|
4473
|
-
records.push({
|
|
4474
|
-
id: header.id,
|
|
4475
|
-
projectDir: project.name,
|
|
4482
|
+
return sessions.filter((session) => session.isDirectory).map((session) => ({ project, session }));
|
|
4483
|
+
});
|
|
4484
|
+
throwIfAborted(signal);
|
|
4485
|
+
const entries = perProject.flat();
|
|
4486
|
+
const workspaceTitles = /* @__PURE__ */ new Map();
|
|
4487
|
+
const workspaceTitleOf = (cwd) => {
|
|
4488
|
+
if (cwd === void 0) return void 0;
|
|
4489
|
+
if (!workspaceTitles.has(cwd)) workspaceTitles.set(cwd, deps.resolveWorkspaceTitle?.(cwd));
|
|
4490
|
+
return workspaceTitles.get(cwd);
|
|
4491
|
+
};
|
|
4492
|
+
return mapWithLimit(entries, INVENTORY_CONCURRENCY, async ({ project, session }) => {
|
|
4493
|
+
throwIfAborted(signal);
|
|
4494
|
+
const dir = joinPath(joinPath(sessionsRoot, project), session.name);
|
|
4495
|
+
const contents = await deps.listDir(dir);
|
|
4496
|
+
let bytes = 0;
|
|
4497
|
+
let updatedAtMs = session.mtimeMs;
|
|
4498
|
+
for (const item of contents) {
|
|
4499
|
+
if (item.isDirectory) continue;
|
|
4500
|
+
bytes += item.bytes;
|
|
4501
|
+
if (item.mtimeMs > updatedAtMs) updatedAtMs = item.mtimeMs;
|
|
4502
|
+
}
|
|
4503
|
+
const header = await deps.readHeader(dir);
|
|
4504
|
+
if (header === void 0) {
|
|
4505
|
+
return {
|
|
4506
|
+
id: session.name,
|
|
4507
|
+
projectDir: project,
|
|
4476
4508
|
segment: session.name,
|
|
4477
|
-
cwd:
|
|
4478
|
-
title,
|
|
4479
|
-
workspaceTitle:
|
|
4480
|
-
parentSessionId:
|
|
4509
|
+
cwd: void 0,
|
|
4510
|
+
title: void 0,
|
|
4511
|
+
workspaceTitle: void 0,
|
|
4512
|
+
parentSessionId: void 0,
|
|
4481
4513
|
bytes,
|
|
4482
4514
|
updatedAtMs,
|
|
4483
|
-
live:
|
|
4484
|
-
running:
|
|
4485
|
-
classification:
|
|
4486
|
-
}
|
|
4515
|
+
live: false,
|
|
4516
|
+
running: false,
|
|
4517
|
+
classification: "unreadable"
|
|
4518
|
+
};
|
|
4487
4519
|
}
|
|
4488
|
-
|
|
4489
|
-
|
|
4520
|
+
const title = header.title ?? deps.resolveSessionTitle?.(header);
|
|
4521
|
+
return {
|
|
4522
|
+
id: header.id,
|
|
4523
|
+
projectDir: project,
|
|
4524
|
+
segment: session.name,
|
|
4525
|
+
cwd: header.cwd,
|
|
4526
|
+
title,
|
|
4527
|
+
workspaceTitle: workspaceTitleOf(header.cwd),
|
|
4528
|
+
parentSessionId: header.parentSessionId,
|
|
4529
|
+
bytes,
|
|
4530
|
+
updatedAtMs,
|
|
4531
|
+
live: deps.isLive(header.id),
|
|
4532
|
+
running: deps.isRunning(header.id),
|
|
4533
|
+
classification: header.parentSessionId === void 0 ? "normal" : "subsession"
|
|
4534
|
+
};
|
|
4535
|
+
});
|
|
4490
4536
|
};
|
|
4491
|
-
return { deps, sessionsRoot, binRoot,
|
|
4537
|
+
return { deps, sessionsRoot, binRoot, indexPath, serialized, entryDir, readIndex, writeIndex, inventory, throwIfAborted };
|
|
4492
4538
|
}
|
|
4493
4539
|
function normalizePathKey(path) {
|
|
4494
|
-
return path
|
|
4540
|
+
return foldPathKey(path);
|
|
4495
4541
|
}
|
|
4496
4542
|
|
|
4497
4543
|
// src/host/app/session-admin/session-admin-delete.ts
|
|
4498
4544
|
init_errors();
|
|
4499
4545
|
function createSessionAdminDelete(ctx) {
|
|
4500
|
-
const { deps, sessionsRoot, serialized, entryDir, readIndex, writeIndex, inventory } = ctx;
|
|
4546
|
+
const { deps, sessionsRoot, serialized, entryDir, readIndex, writeIndex, inventory, throwIfAborted } = ctx;
|
|
4501
4547
|
const previewDelete = async (ids, options = {}) => {
|
|
4502
4548
|
const records = await inventory(options);
|
|
4503
4549
|
return planDelete(records, {
|
|
@@ -4505,10 +4551,11 @@ function createSessionAdminDelete(ctx) {
|
|
|
4505
4551
|
...options.keep === void 0 ? {} : { keep: options.keep }
|
|
4506
4552
|
});
|
|
4507
4553
|
};
|
|
4508
|
-
const deletePermanently = async (decision, byId) => {
|
|
4554
|
+
const deletePermanently = async (decision, byId, options = {}) => {
|
|
4509
4555
|
let bytes = 0;
|
|
4510
4556
|
const deleted = [];
|
|
4511
4557
|
for (const id of decision.delete) {
|
|
4558
|
+
throwIfAborted(options.signal);
|
|
4512
4559
|
const record2 = byId.get(id);
|
|
4513
4560
|
if (record2 === void 0) continue;
|
|
4514
4561
|
if (deps.isRunning(id)) {
|
|
@@ -4523,7 +4570,7 @@ function createSessionAdminDelete(ctx) {
|
|
|
4523
4570
|
}
|
|
4524
4571
|
return { mode: "permanent", entry: void 0, recoverable: false, verified: true, copied: false, refusals: decision.refusals, bytes };
|
|
4525
4572
|
};
|
|
4526
|
-
const moveIntoBin = async (decision, byId) => {
|
|
4573
|
+
const moveIntoBin = async (decision, byId, options = {}) => {
|
|
4527
4574
|
let bytes = 0;
|
|
4528
4575
|
let entry;
|
|
4529
4576
|
let verified = false;
|
|
@@ -4544,6 +4591,7 @@ function createSessionAdminDelete(ctx) {
|
|
|
4544
4591
|
};
|
|
4545
4592
|
try {
|
|
4546
4593
|
for (const id of decision.delete) {
|
|
4594
|
+
throwIfAborted(options.signal);
|
|
4547
4595
|
const record2 = byId.get(id);
|
|
4548
4596
|
if (record2 === void 0) continue;
|
|
4549
4597
|
if (deps.isRunning(id)) {
|
|
@@ -4631,7 +4679,7 @@ function createSessionAdminDelete(ctx) {
|
|
|
4631
4679
|
...options.keep === void 0 ? {} : { keep: options.keep }
|
|
4632
4680
|
});
|
|
4633
4681
|
const byId = new Map(records.map((record2) => [record2.id, record2]));
|
|
4634
|
-
return mode === "permanent" ? deletePermanently(decision, byId) : moveIntoBin(decision, byId);
|
|
4682
|
+
return mode === "permanent" ? deletePermanently(decision, byId, options) : moveIntoBin(decision, byId, options);
|
|
4635
4683
|
});
|
|
4636
4684
|
const purgeEntry = async (entryId) => {
|
|
4637
4685
|
const entries = await readIndex();
|
|
@@ -12166,6 +12214,27 @@ var RemoteIndex = class {
|
|
|
12166
12214
|
* root is correct, just slower.
|
|
12167
12215
|
*/
|
|
12168
12216
|
#scannedOnce = /* @__PURE__ */ new Set();
|
|
12217
|
+
/**
|
|
12218
|
+
* In-flight enumerations, so a burst of callers on a cold root does **one** scan.
|
|
12219
|
+
*
|
|
12220
|
+
* 审计 2026-11-05 ts.md P1:`enumerate` 没有 single-flight——同一次键入可能同时发出 `index.scan` 与
|
|
12221
|
+
* `index.find`(file-reference 一次 list 最多两个枚举,可指向同一 root),于是两次远端扫描都跑、
|
|
12222
|
+
* 结果只留一份。键里带 signal 身份:不同请求的取消不能互相传染(A 的 abort 不该杀掉 B 的扫描)。
|
|
12223
|
+
*/
|
|
12224
|
+
#inFlight = /* @__PURE__ */ new Map();
|
|
12225
|
+
/** Stable identity per `AbortSignal`, only used to keep in-flight keys apart. */
|
|
12226
|
+
#signalIds = /* @__PURE__ */ new WeakMap();
|
|
12227
|
+
#nextSignalId = 0;
|
|
12228
|
+
#inFlightKey(root, signal) {
|
|
12229
|
+
if (signal === void 0) return `${root}\0`;
|
|
12230
|
+
let id = this.#signalIds.get(signal);
|
|
12231
|
+
if (id === void 0) {
|
|
12232
|
+
this.#nextSignalId += 1;
|
|
12233
|
+
id = this.#nextSignalId;
|
|
12234
|
+
this.#signalIds.set(signal, id);
|
|
12235
|
+
}
|
|
12236
|
+
return `${root}\0${id}`;
|
|
12237
|
+
}
|
|
12169
12238
|
constructor(options) {
|
|
12170
12239
|
this.#tiers = options.tiers;
|
|
12171
12240
|
this.#ttlMs = options.ttlMs ?? DEFAULTS.ttlMs;
|
|
@@ -12195,9 +12264,20 @@ var RemoteIndex = class {
|
|
|
12195
12264
|
};
|
|
12196
12265
|
}
|
|
12197
12266
|
}
|
|
12198
|
-
const
|
|
12199
|
-
|
|
12200
|
-
return
|
|
12267
|
+
const key = this.#inFlightKey(root, options.signal);
|
|
12268
|
+
const running = this.#inFlight.get(key);
|
|
12269
|
+
if (running !== void 0) return running;
|
|
12270
|
+
const attempt = (async () => {
|
|
12271
|
+
const result = await this.#enumerateFresh(root, maxEntries, options.budgetMs ?? this.#budgetMs, options.signal);
|
|
12272
|
+
if (result.cacheable) this.#remember(root, result);
|
|
12273
|
+
return result;
|
|
12274
|
+
})();
|
|
12275
|
+
this.#inFlight.set(key, attempt);
|
|
12276
|
+
try {
|
|
12277
|
+
return await attempt;
|
|
12278
|
+
} finally {
|
|
12279
|
+
if (this.#inFlight.get(key) === attempt) this.#inFlight.delete(key);
|
|
12280
|
+
}
|
|
12201
12281
|
}
|
|
12202
12282
|
/**
|
|
12203
12283
|
* Completion for a partially typed `@` query.
|
|
@@ -12712,6 +12792,129 @@ import { join as join9 } from "node:path";
|
|
|
12712
12792
|
import { homedir as homedir2 } from "node:os";
|
|
12713
12793
|
import { createRequire } from "node:module";
|
|
12714
12794
|
import "@deepseek-ai/cordis";
|
|
12795
|
+
|
|
12796
|
+
// src/host/domain/roots/ssh-config.ts
|
|
12797
|
+
function stripComment(line) {
|
|
12798
|
+
let inQuotes = false;
|
|
12799
|
+
for (let index = 0; index < line.length; index++) {
|
|
12800
|
+
const char = line[index];
|
|
12801
|
+
if (char === '"') inQuotes = !inQuotes;
|
|
12802
|
+
else if (char === "#" && !inQuotes) return line.slice(0, index);
|
|
12803
|
+
}
|
|
12804
|
+
return line;
|
|
12805
|
+
}
|
|
12806
|
+
function unquote(value) {
|
|
12807
|
+
if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) return value.slice(1, -1);
|
|
12808
|
+
return value;
|
|
12809
|
+
}
|
|
12810
|
+
function splitKeyValue(line) {
|
|
12811
|
+
const match = /^([A-Za-z][A-Za-z0-9]*)\s*(?:=|\s)\s*(.*)$/.exec(line);
|
|
12812
|
+
if (match === null) return void 0;
|
|
12813
|
+
const key = match[1];
|
|
12814
|
+
const value = match[2];
|
|
12815
|
+
if (key === void 0 || value === void 0) return void 0;
|
|
12816
|
+
return [key.toLowerCase(), unquote(value.trim())];
|
|
12817
|
+
}
|
|
12818
|
+
function parseSshConfig(text8) {
|
|
12819
|
+
const entries = [];
|
|
12820
|
+
let current;
|
|
12821
|
+
const lines = text8.split(/\r?\n/);
|
|
12822
|
+
for (let index = 0; index < lines.length; index++) {
|
|
12823
|
+
const raw = lines[index] ?? "";
|
|
12824
|
+
const line = stripComment(raw).trim();
|
|
12825
|
+
if (line === "") continue;
|
|
12826
|
+
const pair = splitKeyValue(line);
|
|
12827
|
+
if (pair === void 0) continue;
|
|
12828
|
+
const [key, value] = pair;
|
|
12829
|
+
if (key === "host") {
|
|
12830
|
+
const patterns = value.split(/\s+/).filter((part) => part !== "");
|
|
12831
|
+
if (patterns.length === 0) continue;
|
|
12832
|
+
current = { patterns, identityFiles: [], includes: [], line: index + 1 };
|
|
12833
|
+
entries.push(current);
|
|
12834
|
+
continue;
|
|
12835
|
+
}
|
|
12836
|
+
if (current === void 0) continue;
|
|
12837
|
+
switch (key) {
|
|
12838
|
+
case "hostname":
|
|
12839
|
+
current.hostName ??= value;
|
|
12840
|
+
break;
|
|
12841
|
+
case "user":
|
|
12842
|
+
current.user ??= value;
|
|
12843
|
+
break;
|
|
12844
|
+
case "port": {
|
|
12845
|
+
if (current.port !== void 0) break;
|
|
12846
|
+
const port = Number.parseInt(value, 10);
|
|
12847
|
+
if (Number.isInteger(port) && port > 0 && port <= 65535) current.port = port;
|
|
12848
|
+
break;
|
|
12849
|
+
}
|
|
12850
|
+
case "identityfile":
|
|
12851
|
+
current.identityFiles.push(value);
|
|
12852
|
+
break;
|
|
12853
|
+
case "proxyjump":
|
|
12854
|
+
current.proxyJump ??= value.toLowerCase() === "none" ? "" : value;
|
|
12855
|
+
break;
|
|
12856
|
+
case "include":
|
|
12857
|
+
current.includes.push(value);
|
|
12858
|
+
break;
|
|
12859
|
+
default:
|
|
12860
|
+
break;
|
|
12861
|
+
}
|
|
12862
|
+
}
|
|
12863
|
+
return entries.map((entry) => ({
|
|
12864
|
+
patterns: entry.patterns,
|
|
12865
|
+
...entry.hostName === void 0 ? {} : { hostName: entry.hostName },
|
|
12866
|
+
...entry.user === void 0 ? {} : { user: entry.user },
|
|
12867
|
+
...entry.port === void 0 ? {} : { port: entry.port },
|
|
12868
|
+
identityFiles: entry.identityFiles,
|
|
12869
|
+
...entry.proxyJump === void 0 || entry.proxyJump === "" ? {} : { proxyJump: entry.proxyJump },
|
|
12870
|
+
includes: entry.includes,
|
|
12871
|
+
line: entry.line
|
|
12872
|
+
}));
|
|
12873
|
+
}
|
|
12874
|
+
function isConcretePattern(pattern) {
|
|
12875
|
+
return !/[*?!]/.test(pattern);
|
|
12876
|
+
}
|
|
12877
|
+
function expandHome(path, home) {
|
|
12878
|
+
if (path === "~") return home;
|
|
12879
|
+
if (path.startsWith("~/") || path.startsWith("~\\")) return `${home.replace(/[\\/]+$/, "")}${path.slice(1)}`;
|
|
12880
|
+
return path;
|
|
12881
|
+
}
|
|
12882
|
+
function importableEntries(entries, options = {}) {
|
|
12883
|
+
const home = options.home ?? "";
|
|
12884
|
+
const excluded = new Set(options.excluded ?? []);
|
|
12885
|
+
const seen = /* @__PURE__ */ new Set();
|
|
12886
|
+
const drafts = [];
|
|
12887
|
+
const skipped = [];
|
|
12888
|
+
for (const entry of entries) {
|
|
12889
|
+
for (const pattern of entry.patterns) {
|
|
12890
|
+
if (!isConcretePattern(pattern)) {
|
|
12891
|
+
skipped.push({ pattern, reason: "wildcard", line: entry.line });
|
|
12892
|
+
continue;
|
|
12893
|
+
}
|
|
12894
|
+
if (seen.has(pattern)) {
|
|
12895
|
+
skipped.push({ pattern, reason: "duplicate", line: entry.line });
|
|
12896
|
+
continue;
|
|
12897
|
+
}
|
|
12898
|
+
if (excluded.has(pattern)) {
|
|
12899
|
+
skipped.push({ pattern, reason: "already-imported", line: entry.line });
|
|
12900
|
+
continue;
|
|
12901
|
+
}
|
|
12902
|
+
seen.add(pattern);
|
|
12903
|
+
const identityFile = entry.identityFiles[0];
|
|
12904
|
+
drafts.push({
|
|
12905
|
+
alias: pattern,
|
|
12906
|
+
hostName: entry.hostName ?? pattern,
|
|
12907
|
+
...entry.user === void 0 ? {} : { user: entry.user },
|
|
12908
|
+
...entry.port === void 0 ? {} : { port: entry.port },
|
|
12909
|
+
...identityFile === void 0 ? {} : { identityFile: home === "" ? identityFile : expandHome(identityFile, home) },
|
|
12910
|
+
...entry.proxyJump === void 0 ? {} : { proxyJump: entry.proxyJump }
|
|
12911
|
+
});
|
|
12912
|
+
}
|
|
12913
|
+
}
|
|
12914
|
+
return { drafts, skipped };
|
|
12915
|
+
}
|
|
12916
|
+
|
|
12917
|
+
// src/wiring/helpers.ts
|
|
12715
12918
|
init_errors();
|
|
12716
12919
|
init_ssh2_transport();
|
|
12717
12920
|
|
|
@@ -12800,7 +13003,7 @@ async function resolveAuth(ctx, profile) {
|
|
|
12800
13003
|
return agent === void 0 ? {} : { agent };
|
|
12801
13004
|
}
|
|
12802
13005
|
if (spec.kind === "key") {
|
|
12803
|
-
const path =
|
|
13006
|
+
const path = expandHome2(spec.keyPath);
|
|
12804
13007
|
const privateKey = await readFile4(path).catch((error) => {
|
|
12805
13008
|
const message = error instanceof Error ? error.message : String(error);
|
|
12806
13009
|
throw new SshDevError("AUTH_FAILED", `\u65E0\u6CD5\u8BFB\u53D6\u79C1\u94A5 ${path}\uFF1A${message}`, { path });
|
|
@@ -12832,10 +13035,8 @@ function isSsh2Available() {
|
|
|
12832
13035
|
return false;
|
|
12833
13036
|
}
|
|
12834
13037
|
}
|
|
12835
|
-
function
|
|
12836
|
-
|
|
12837
|
-
if (path.startsWith("~/") || path.startsWith("~\\")) return join9(homedir2(), path.slice(2));
|
|
12838
|
-
return path;
|
|
13038
|
+
function expandHome2(path) {
|
|
13039
|
+
return expandHome(path, homedir2());
|
|
12839
13040
|
}
|
|
12840
13041
|
var readSessionHeader = createDiskSessionHeaderReader();
|
|
12841
13042
|
|
|
@@ -12928,8 +13129,8 @@ function wireConnections(ctx, config) {
|
|
|
12928
13129
|
hostKeyPolicy: profile.hostKeyPolicy,
|
|
12929
13130
|
// A connection that names a key must use it: the alias transport otherwise relies entirely on the
|
|
12930
13131
|
// user's own ~/.ssh/config, which silently ignores the plugin's `keyPath`.
|
|
12931
|
-
...profile.auth.kind === "key" ? { identityFile:
|
|
12932
|
-
...limits.knownHostsFile === void 0 ? {} : { knownHostsFile:
|
|
13132
|
+
...profile.auth.kind === "key" ? { identityFile: expandHome2(profile.auth.keyPath) } : {},
|
|
13133
|
+
...limits.knownHostsFile === void 0 ? {} : { knownHostsFile: expandHome2(limits.knownHostsFile) },
|
|
12933
13134
|
// The connection's `user` must reach ssh; otherwise a bare host falls back to the local username.
|
|
12934
13135
|
...profile.user === "" ? {} : { user: profile.user },
|
|
12935
13136
|
// Password connections authenticate through an SSH_ASKPASS helper: they must stop forbidding prompts
|
|
@@ -13022,8 +13223,8 @@ function wireConnections(ctx, config) {
|
|
|
13022
13223
|
sshArgv: sshPrefix("ssh", {
|
|
13023
13224
|
alias: profile.alias ?? profile.host,
|
|
13024
13225
|
user: profile.user,
|
|
13025
|
-
...profile.auth.kind === "key" ? { identityFile:
|
|
13026
|
-
...limits.knownHostsFile === void 0 ? {} : { knownHostsFile:
|
|
13226
|
+
...profile.auth.kind === "key" ? { identityFile: expandHome2(profile.auth.keyPath) } : {},
|
|
13227
|
+
...limits.knownHostsFile === void 0 ? {} : { knownHostsFile: expandHome2(limits.knownHostsFile) },
|
|
13027
13228
|
hostKeyPolicy: profile.hostKeyPolicy,
|
|
13028
13229
|
connectTimeoutMs: limits.connectTimeoutMs ?? profile.connectTimeoutMs,
|
|
13029
13230
|
...passwordOptions.batchMode === void 0 ? {} : { batchMode: passwordOptions.batchMode }
|
|
@@ -13120,11 +13321,11 @@ function wireConnections(ctx, config) {
|
|
|
13120
13321
|
port: profile.port,
|
|
13121
13322
|
user: profile.user,
|
|
13122
13323
|
authKind: profile.auth.kind,
|
|
13123
|
-
...profile.auth.kind === "key" ? { identityFile:
|
|
13324
|
+
...profile.auth.kind === "key" ? { identityFile: expandHome2(profile.auth.keyPath) } : {},
|
|
13124
13325
|
connectTimeoutMs: profile.connectTimeoutMs
|
|
13125
13326
|
},
|
|
13126
13327
|
{
|
|
13127
|
-
...limits.knownHostsFile === void 0 ? {} : { knownHostsFile:
|
|
13328
|
+
...limits.knownHostsFile === void 0 ? {} : { knownHostsFile: expandHome2(limits.knownHostsFile) },
|
|
13128
13329
|
...limits.connectTimeoutMs === void 0 ? {} : { connectTimeoutMs: limits.connectTimeoutMs }
|
|
13129
13330
|
}
|
|
13130
13331
|
);
|
|
@@ -13277,8 +13478,12 @@ import { mkdir as mkdir7 } from "node:fs/promises";
|
|
|
13277
13478
|
// src/host/integration/remote/remote-face.ts
|
|
13278
13479
|
import { Service } from "@deepseek-ai/cordis";
|
|
13279
13480
|
import { bindTypertRemote } from "@deepseek-ai/dsh-typert-protocol";
|
|
13481
|
+
|
|
13482
|
+
// src/shared/remote-identity.ts
|
|
13280
13483
|
var SSH_DEV_REMOTE_NAMESPACE = "sshdev";
|
|
13281
13484
|
var SSH_DEV_REMOTE_SERVICE = "sshDevRemote";
|
|
13485
|
+
|
|
13486
|
+
// src/host/integration/remote/remote-face.ts
|
|
13282
13487
|
var SSH_DEV_REMOTE_CONNECTIONS_METHOD = "connections";
|
|
13283
13488
|
var NOT_CONNECTED_NOTE = "\u5C1A\u672A\u5EFA\u7ACB\u8FDE\u63A5\uFF1A\u4F20\u8F93\u51B3\u7B56\u5728\u9996\u6B21\u8FDE\u63A5\u65F6\u786E\u5B9A\u3002";
|
|
13284
13489
|
function buildConnectionStatusRows(source) {
|
|
@@ -13541,7 +13746,10 @@ function registerSessionTools(ctx, service) {
|
|
|
13541
13746
|
};
|
|
13542
13747
|
}
|
|
13543
13748
|
|
|
13544
|
-
// src/host/
|
|
13749
|
+
// src/host/integration/tools/session-directory-tools.ts
|
|
13750
|
+
import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
|
|
13751
|
+
|
|
13752
|
+
// src/host/app/roots/session-directories.ts
|
|
13545
13753
|
init_errors();
|
|
13546
13754
|
|
|
13547
13755
|
// src/host/app/roots/user-roots.ts
|
|
@@ -13951,10 +14159,10 @@ function createUserRootsStore(options) {
|
|
|
13951
14159
|
cachedLoad = parsed;
|
|
13952
14160
|
return parsed;
|
|
13953
14161
|
};
|
|
13954
|
-
const write = async (next) => {
|
|
14162
|
+
const write = async (next, preparedText) => {
|
|
13955
14163
|
const validated = validateState(next);
|
|
13956
14164
|
const temporary = `${filePath}.${process.pid}-${++tempSequence2}.tmp`;
|
|
13957
|
-
const text8 = serializeUserRoots(validated);
|
|
14165
|
+
const text8 = preparedText ?? serializeUserRoots(validated);
|
|
13958
14166
|
await io.mkdir(options.dir);
|
|
13959
14167
|
try {
|
|
13960
14168
|
await io.writeFile(temporary, text8);
|
|
@@ -13974,16 +14182,18 @@ function createUserRootsStore(options) {
|
|
|
13974
14182
|
assertUserRootsWritable(await load());
|
|
13975
14183
|
return write(next);
|
|
13976
14184
|
});
|
|
14185
|
+
const applyTransition = async (transition) => {
|
|
14186
|
+
const loaded = await load();
|
|
14187
|
+
assertUserRootsWritable(loaded);
|
|
14188
|
+
const wanted = transition(loaded.state);
|
|
14189
|
+
if (wanted === loaded.state) return loaded.state;
|
|
14190
|
+
const wantedText = serializeUserRoots(wanted);
|
|
14191
|
+
if (cachedRaw !== void 0 && wantedText === cachedRaw) return loaded.state;
|
|
14192
|
+
return write(wanted, wantedText);
|
|
14193
|
+
};
|
|
13977
14194
|
const mutate = (transition) => {
|
|
13978
14195
|
const locked = options.withLock;
|
|
13979
|
-
|
|
13980
|
-
const loaded = await load();
|
|
13981
|
-
assertUserRootsWritable(loaded);
|
|
13982
|
-
const wanted = transition(loaded.state);
|
|
13983
|
-
if (serializeUserRoots(wanted) === serializeUserRoots(loaded.state)) return loaded.state;
|
|
13984
|
-
return write(wanted);
|
|
13985
|
-
});
|
|
13986
|
-
return locked === void 0 ? run() : locked(run);
|
|
14196
|
+
return serialize(() => locked === void 0 ? applyTransition(transition) : locked(() => applyTransition(transition)));
|
|
13987
14197
|
};
|
|
13988
14198
|
return {
|
|
13989
14199
|
filePath,
|
|
@@ -14137,6 +14347,7 @@ function isMissingFile(error) {
|
|
|
14137
14347
|
}
|
|
14138
14348
|
|
|
14139
14349
|
// src/host/app/roots/roots-api-http.ts
|
|
14350
|
+
init_errors();
|
|
14140
14351
|
async function readBody(req, maxBytes) {
|
|
14141
14352
|
const chunks = [];
|
|
14142
14353
|
let total = 0;
|
|
@@ -14197,7 +14408,10 @@ var STATUS_BY_CODE = {
|
|
|
14197
14408
|
TRASH_NOT_FOUND: 404,
|
|
14198
14409
|
TRANSFER_CONFLICT: 409,
|
|
14199
14410
|
SESSION_ACTIVE: 409,
|
|
14200
|
-
REMOTE_UNSUPPORTED: 501
|
|
14411
|
+
REMOTE_UNSUPPORTED: 501,
|
|
14412
|
+
// 调用方主动取消(客户端断开/关掉面板)。499 是 nginx 的 "client closed request" 约定:它不是一个错误,
|
|
14413
|
+
// 只是说明"没人等这个回答了";映射成 500 会让日志里出现一堆假故障。
|
|
14414
|
+
CANCELLED: 499
|
|
14201
14415
|
};
|
|
14202
14416
|
function codeOf(error) {
|
|
14203
14417
|
const code = asRecord2(error)?.["code"];
|
|
@@ -14275,80 +14489,407 @@ function connectionIdFromAlias(alias) {
|
|
|
14275
14489
|
return isValidConnectionId(folded) ? folded : void 0;
|
|
14276
14490
|
}
|
|
14277
14491
|
|
|
14278
|
-
// src/
|
|
14279
|
-
var
|
|
14280
|
-
var
|
|
14281
|
-
function
|
|
14282
|
-
|
|
14283
|
-
|
|
14284
|
-
|
|
14285
|
-
|
|
14286
|
-
|
|
14287
|
-
|
|
14288
|
-
|
|
14289
|
-
|
|
14290
|
-
|
|
14291
|
-
|
|
14292
|
-
|
|
14293
|
-
|
|
14294
|
-
|
|
14295
|
-
|
|
14296
|
-
|
|
14297
|
-
"test-connection",
|
|
14298
|
-
"remove-connection",
|
|
14299
|
-
"list-remote",
|
|
14300
|
-
"list-local",
|
|
14301
|
-
"list-ssh-config",
|
|
14302
|
-
"import-ssh-config",
|
|
14303
|
-
"mkdir-remote",
|
|
14304
|
-
"rename-remote",
|
|
14305
|
-
"remove-remote",
|
|
14306
|
-
"list-transfers",
|
|
14307
|
-
"clear-transfers"
|
|
14308
|
-
];
|
|
14309
|
-
var ROOT_FIELDS = ["location", "path", "connectionId", "label", "writable", "workspaces", "sessions"];
|
|
14310
|
-
var CONNECTION_FIELDS = ["id", "host", "user", "port", "authKind", "keyPath", "credentialKey", "transport"];
|
|
14311
|
-
var UPDATE_CONNECTION_FIELDS = ["host", "user", "port", "authKind", "keyPath", "credentialKey", "transport"];
|
|
14312
|
-
function trustVerdict(req) {
|
|
14313
|
-
const host = headerOf(req, "host");
|
|
14314
|
-
if (host === void 0) return "no-host";
|
|
14315
|
-
const hostUrl = parseAuthority(host);
|
|
14316
|
-
if (hostUrl === void 0) return "bad-host";
|
|
14317
|
-
if (headerOf(req, "sec-fetch-site") === "cross-site") return "cross-site";
|
|
14318
|
-
const origin = headerOf(req, "origin");
|
|
14319
|
-
if (origin === void 0) return "ok";
|
|
14320
|
-
const trimmed = origin.trim();
|
|
14321
|
-
if (trimmed === "" || trimmed === "null") return "bad-origin";
|
|
14322
|
-
let originUrl;
|
|
14323
|
-
try {
|
|
14324
|
-
originUrl = new URL(trimmed);
|
|
14325
|
-
} catch {
|
|
14326
|
-
return "bad-origin";
|
|
14492
|
+
// src/host/app/roots/session-directories.ts
|
|
14493
|
+
var SESSION_DIRECTORY_MAX_ROOTS = 32;
|
|
14494
|
+
var SYSTEM_TREES = ["/etc", "/usr", "/bin", "/sbin", "/lib", "/lib64", "/boot", "/sys", "/proc", "/dev", "/var", "/root"];
|
|
14495
|
+
function directoryRefusal(request) {
|
|
14496
|
+
const trimmed = request.path.trim();
|
|
14497
|
+
if (trimmed === "") return "\u76EE\u5F55\u8DEF\u5F84\u4E3A\u7A7A\u3002";
|
|
14498
|
+
if (request.location === "remote") {
|
|
14499
|
+
if (request.connectionId === void 0 || request.connectionId === "") {
|
|
14500
|
+
return "\u8FDC\u7AEF\u76EE\u5F55\u9700\u8981 connectionId\uFF08\u5148\u786E\u8BA4\u76EE\u6807\u8FDE\u63A5\uFF09\u3002";
|
|
14501
|
+
}
|
|
14502
|
+
if (!trimmed.startsWith("/")) return `\u8FDC\u7AEF\u76EE\u5F55\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84\uFF1A${trimmed}`;
|
|
14503
|
+
let normalized2;
|
|
14504
|
+
try {
|
|
14505
|
+
normalized2 = normalizeRemotePath(trimmed);
|
|
14506
|
+
} catch (error) {
|
|
14507
|
+
return error instanceof Error ? error.message : `\u8FDC\u7AEF\u8DEF\u5F84\u4E0D\u5408\u6CD5\uFF1A${trimmed}`;
|
|
14508
|
+
}
|
|
14509
|
+
if (normalized2 === "/") return "\u4E0D\u5141\u8BB8\u628A\u8FDC\u7AEF\u7684\u6839\u76EE\u5F55 / \u6DFB\u52A0\u5230\u4F1A\u8BDD\u3002";
|
|
14510
|
+
return void 0;
|
|
14327
14511
|
}
|
|
14328
|
-
|
|
14329
|
-
if (
|
|
14330
|
-
if (
|
|
14331
|
-
|
|
14332
|
-
}
|
|
14333
|
-
function trustMessage(verdict) {
|
|
14334
|
-
switch (verdict) {
|
|
14335
|
-
case "no-host":
|
|
14336
|
-
return "\u8BF7\u6C42\u7F3A\u5C11 Host \u5934\uFF0C\u62D2\u7EDD";
|
|
14337
|
-
case "bad-host":
|
|
14338
|
-
return "Host \u5934\u65E0\u6CD5\u89E3\u6790\uFF0C\u62D2\u7EDD";
|
|
14339
|
-
case "cross-site":
|
|
14340
|
-
return "\u8DE8\u7AD9\u8BF7\u6C42\uFF08Sec-Fetch-Site: cross-site\uFF09\u88AB\u62D2\u7EDD";
|
|
14341
|
-
case "bad-origin":
|
|
14342
|
-
return "\u8BF7\u6C42\u7684 Origin \u4E0E Host \u4E0D\u540C\u6E90\uFF0C\u62D2\u7EDD";
|
|
14343
|
-
default:
|
|
14344
|
-
return "ok";
|
|
14512
|
+
const normalized = trimmed.replace(/\/+$/u, "") || "/";
|
|
14513
|
+
if (!isAbsoluteLocalPath(normalized)) return `\u672C\u5730\u76EE\u5F55\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84\uFF1A${trimmed}`;
|
|
14514
|
+
if (normalized === "/") return "\u4E0D\u5141\u8BB8\u628A\u672C\u673A\u7684\u6839\u76EE\u5F55 / \u6DFB\u52A0\u5230\u4F1A\u8BDD\u3002";
|
|
14515
|
+
if (SYSTEM_TREES.some((tree) => normalized === tree || localPathContains(tree, normalized))) {
|
|
14516
|
+
return `\u4E0D\u5141\u8BB8\u628A\u7CFB\u7EDF\u76EE\u5F55\u6DFB\u52A0\u5230\u4F1A\u8BDD\uFF1A${normalized}`;
|
|
14345
14517
|
}
|
|
14518
|
+
const workspace = request.workspacePath.trim();
|
|
14519
|
+
if (workspace !== "") {
|
|
14520
|
+
if (normalized === workspace || localPathContains(normalized, workspace)) {
|
|
14521
|
+
return `\u4E0D\u5141\u8BB8\u6DFB\u52A0\u5F53\u524D\u5DE5\u4F5C\u533A\u7684\u7956\u5148\u76EE\u5F55\uFF08${normalized}\uFF09\uFF1A\u90A3\u4F1A\u628A\u6574\u68F5\u7236\u6811\u7EB3\u5165\u53EF\u5199\u8303\u56F4\u3002\u8BF7\u6DFB\u52A0\u5DE5\u4F5C\u533A\u5185\u90E8\u6216\u4E0E\u5B83\u5E76\u5217\u7684\u76EE\u5F55\u3002`;
|
|
14522
|
+
}
|
|
14523
|
+
}
|
|
14524
|
+
return void 0;
|
|
14525
|
+
}
|
|
14526
|
+
function createSessionDirectories(deps) {
|
|
14527
|
+
return {
|
|
14528
|
+
async add(request) {
|
|
14529
|
+
const refusal = directoryRefusal(request);
|
|
14530
|
+
if (refusal !== void 0) return { ok: false, reason: refusal };
|
|
14531
|
+
if (request.workspacePath.trim() === "") return { ok: false, reason: "\u62FF\u4E0D\u5230\u5F53\u524D\u5DE5\u4F5C\u533A\u8DEF\u5F84\uFF0C\u65E0\u6CD5\u6DFB\u52A0\u76EE\u5F55\u3002" };
|
|
14532
|
+
if (request.sessionId.trim() === "") return { ok: false, reason: "\u62FF\u4E0D\u5230\u5F53\u524D\u4F1A\u8BDD id\uFF0C\u65E0\u6CD5\u6DFB\u52A0\u76EE\u5F55\u3002" };
|
|
14533
|
+
const entry = {
|
|
14534
|
+
location: request.location,
|
|
14535
|
+
path: request.path.trim(),
|
|
14536
|
+
...request.connectionId === void 0 ? {} : { connectionId: request.connectionId },
|
|
14537
|
+
writable: true,
|
|
14538
|
+
workspaces: [request.workspacePath],
|
|
14539
|
+
sessions: [request.sessionId]
|
|
14540
|
+
};
|
|
14541
|
+
const key = userRootKey(entry);
|
|
14542
|
+
let state;
|
|
14543
|
+
try {
|
|
14544
|
+
state = await deps.mutateUserState((current) => {
|
|
14545
|
+
const existing = findRootInState(current, key);
|
|
14546
|
+
if (existing === void 0) {
|
|
14547
|
+
const scoped = current.roots.filter(
|
|
14548
|
+
(candidate) => (candidate.sessions ?? []).includes(request.sessionId) || (candidate.workspaces ?? []).includes(request.workspacePath)
|
|
14549
|
+
).length;
|
|
14550
|
+
if (scoped >= SESSION_DIRECTORY_MAX_ROOTS) {
|
|
14551
|
+
throw new SshDevError(
|
|
14552
|
+
"ROOT_INVALID",
|
|
14553
|
+
`\u672C\u4F1A\u8BDD\u5DF2\u6DFB\u52A0 ${scoped} \u4E2A\u76EE\u5F55\uFF0C\u8FBE\u5230\u4E0A\u9650 ${SESSION_DIRECTORY_MAX_ROOTS}\uFF1B\u8BF7\u5148\u79FB\u9664\u4E0D\u518D\u9700\u8981\u7684\u76EE\u5F55\u3002`,
|
|
14554
|
+
{ limit: SESSION_DIRECTORY_MAX_ROOTS }
|
|
14555
|
+
);
|
|
14556
|
+
}
|
|
14557
|
+
return addRootToState(current, entry);
|
|
14558
|
+
}
|
|
14559
|
+
return withSessions(withWorkspaces(current, key, [request.workspacePath]), key, [request.sessionId]);
|
|
14560
|
+
});
|
|
14561
|
+
} catch (error) {
|
|
14562
|
+
return { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
|
14563
|
+
}
|
|
14564
|
+
return { ok: true, key, ...await ensureLiveAndReadAnchor(deps, key, state) };
|
|
14565
|
+
},
|
|
14566
|
+
async remove(request) {
|
|
14567
|
+
if (request.sessionId.trim() === "") return { ok: false, reason: "\u62FF\u4E0D\u5230\u5F53\u524D\u4F1A\u8BDD id\uFF0C\u65E0\u6CD5\u79FB\u9664\u76EE\u5F55" };
|
|
14568
|
+
let key = request.key?.trim() ?? "";
|
|
14569
|
+
let before;
|
|
14570
|
+
let state;
|
|
14571
|
+
try {
|
|
14572
|
+
state = await deps.mutateUserState((current) => {
|
|
14573
|
+
const resolved = resolveRemoveKey(current, request);
|
|
14574
|
+
if (resolved === void 0) {
|
|
14575
|
+
throw new SshDevError(
|
|
14576
|
+
"ROOT_INVALID",
|
|
14577
|
+
request.path === void 0 ? `\u6CA1\u6709\u627E\u5230\u8981\u79FB\u9664\u7684\u76EE\u5F55\uFF1A${key}` : `\u672C\u4F1A\u8BDD\u6CA1\u6709\u8FD9\u4E2A\u76EE\u5F55\uFF1A${request.path}`,
|
|
14578
|
+
{ key, path: request.path }
|
|
14579
|
+
);
|
|
14580
|
+
}
|
|
14581
|
+
key = resolved.key;
|
|
14582
|
+
before = resolved.entry;
|
|
14583
|
+
if (!(resolved.entry.sessions ?? []).includes(request.sessionId)) {
|
|
14584
|
+
throw new SshDevError("ROOT_INVALID", "\u53EA\u80FD\u79FB\u9664\u672C\u4F1A\u8BDD\u81EA\u5DF1\u6DFB\u52A0\u7684\u76EE\u5F55\uFF1B\u8FD9\u6761\u4E0D\u662F\u672C\u4F1A\u8BDD\u52A0\u7684\u3002", { key });
|
|
14585
|
+
}
|
|
14586
|
+
return removeRootFromSession(current, key, request.sessionId);
|
|
14587
|
+
});
|
|
14588
|
+
} catch (error) {
|
|
14589
|
+
return { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
|
14590
|
+
}
|
|
14591
|
+
if (before === void 0) return { ok: false, reason: `\u6CA1\u6709\u627E\u5230\u8981\u79FB\u9664\u7684\u76EE\u5F55\uFF1A${key}` };
|
|
14592
|
+
if (!stateHasRoot(state, key)) {
|
|
14593
|
+
const liveKey = await findLiveKey(deps, before);
|
|
14594
|
+
if (liveKey !== void 0) await deps.unregisterRoot(liveKey);
|
|
14595
|
+
return { ok: true, key };
|
|
14596
|
+
}
|
|
14597
|
+
await refreshLiveScope(deps, key, state);
|
|
14598
|
+
return { ok: true, key };
|
|
14599
|
+
}
|
|
14600
|
+
};
|
|
14601
|
+
}
|
|
14602
|
+
function resolveRemoveKey(state, request) {
|
|
14603
|
+
const wantedKey = request.key?.trim();
|
|
14604
|
+
if (wantedKey !== void 0 && wantedKey !== "") {
|
|
14605
|
+
const entry = findRootInState(state, wantedKey);
|
|
14606
|
+
return entry === void 0 ? void 0 : { key: wantedKey, entry };
|
|
14607
|
+
}
|
|
14608
|
+
const path = request.path?.trim();
|
|
14609
|
+
if (path === void 0 || path === "") return void 0;
|
|
14610
|
+
const wanted = request.connectionId === void 0 ? userRootKey({ location: "local", path }) : userRootKey({ location: "remote", path, connectionId: request.connectionId });
|
|
14611
|
+
for (const entry of state.roots) {
|
|
14612
|
+
if (userRootKey(entry) === wanted) return { key: wanted, entry };
|
|
14613
|
+
}
|
|
14614
|
+
if (request.connectionId === void 0) {
|
|
14615
|
+
const matches = state.roots.filter((entry) => entry.path === path);
|
|
14616
|
+
if (matches.length === 1) return { key: userRootKey(matches[0]), entry: matches[0] };
|
|
14617
|
+
}
|
|
14618
|
+
return void 0;
|
|
14619
|
+
}
|
|
14620
|
+
async function ensureLiveAndReadAnchor(deps, key, state) {
|
|
14621
|
+
const persisted = findRootInState(state, key);
|
|
14622
|
+
if (persisted === void 0) return {};
|
|
14623
|
+
const readAnchor = (roots2) => {
|
|
14624
|
+
for (const raw of roots2) {
|
|
14625
|
+
if (liveKeyOfRecord(raw, persisted) === void 0) continue;
|
|
14626
|
+
const source = raw?.root ?? raw;
|
|
14627
|
+
const anchor = source["anchorPath"];
|
|
14628
|
+
if (typeof anchor === "string" && anchor !== "") return { anchorPath: anchor };
|
|
14629
|
+
return {};
|
|
14630
|
+
}
|
|
14631
|
+
return {};
|
|
14632
|
+
};
|
|
14633
|
+
const roots = await Promise.resolve(deps.listRoots());
|
|
14634
|
+
const liveEntry = roots.find((candidate) => liveKeyOfRecord(candidate, persisted) !== void 0);
|
|
14635
|
+
if (liveEntry === void 0) {
|
|
14636
|
+
await deps.registerRoot(persisted);
|
|
14637
|
+
return readAnchor(await Promise.resolve(deps.listRoots()));
|
|
14638
|
+
}
|
|
14639
|
+
if (liveScopeIsCurrent(liveEntry, persisted)) return readAnchor(roots);
|
|
14640
|
+
const liveKey = liveKeyOfRecord(liveEntry, persisted);
|
|
14641
|
+
if (liveKey !== void 0) {
|
|
14642
|
+
await deps.unregisterRoot(liveKey);
|
|
14643
|
+
await deps.registerRoot(persisted);
|
|
14644
|
+
}
|
|
14645
|
+
return readAnchor(await Promise.resolve(deps.listRoots()));
|
|
14646
|
+
}
|
|
14647
|
+
async function findLiveKey(deps, entry) {
|
|
14648
|
+
for (const raw of await Promise.resolve(deps.listRoots())) {
|
|
14649
|
+
const key = liveKeyOfRecord(raw, entry);
|
|
14650
|
+
if (key !== void 0) return key;
|
|
14651
|
+
}
|
|
14652
|
+
return void 0;
|
|
14653
|
+
}
|
|
14654
|
+
async function refreshLiveScope(deps, key, state) {
|
|
14655
|
+
const persisted = findRootInState(state, key);
|
|
14656
|
+
if (persisted === void 0) return;
|
|
14657
|
+
const roots = await Promise.resolve(deps.listRoots());
|
|
14658
|
+
const liveEntry = roots.find((candidate) => liveKeyOfRecord(candidate, persisted) !== void 0);
|
|
14659
|
+
if (liveEntry === void 0) return;
|
|
14660
|
+
if (liveScopeIsCurrent(liveEntry, persisted)) return;
|
|
14661
|
+
const liveKey = liveKeyOfRecord(liveEntry, persisted);
|
|
14662
|
+
if (liveKey === void 0) return;
|
|
14663
|
+
await deps.unregisterRoot(liveKey);
|
|
14664
|
+
await deps.registerRoot(persisted);
|
|
14665
|
+
}
|
|
14666
|
+
function liveScopeIsCurrent(raw, entry) {
|
|
14667
|
+
const outer = raw ?? {};
|
|
14668
|
+
const source = outer.root ?? outer;
|
|
14669
|
+
if (source["shared"] === true) return true;
|
|
14670
|
+
return sameStrings(source["workspaces"], entry.workspaces) && sameStrings(source["sessions"], entry.sessions);
|
|
14671
|
+
}
|
|
14672
|
+
function sameStrings(left, right) {
|
|
14673
|
+
const asSet = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string").sort() : [];
|
|
14674
|
+
const a = asSet(left);
|
|
14675
|
+
const b = asSet(right);
|
|
14676
|
+
return a.length === b.length && a.every((value, index) => value === b[index]);
|
|
14677
|
+
}
|
|
14678
|
+
|
|
14679
|
+
// src/host/integration/tools/session-directory-tools.ts
|
|
14680
|
+
var SESSION_DIRECTORY_TOOL_NAMES = {
|
|
14681
|
+
// **必须以 `remote_` 开头**:插件的严格参数校验网关按这个名字前缀覆盖它的全部工具
|
|
14682
|
+
//(`strict-arguments.ts` 的 `STRICT_TOOL_PREFIX`),换个前缀就会静默逃过参数校验。
|
|
14683
|
+
add: "remote_add_directory",
|
|
14684
|
+
remove: "remote_remove_directory"
|
|
14685
|
+
};
|
|
14686
|
+
function sessionDirectoryToolDefinitions(deps) {
|
|
14687
|
+
return [
|
|
14688
|
+
defineTool2({
|
|
14689
|
+
name: SESSION_DIRECTORY_TOOL_NAMES.add,
|
|
14690
|
+
description: `Add a directory to **this conversation**, so its file tools and \`@\` completion can use it. The directory becomes part of the session workspace: under \`workspace-write\` it is readable and writable, and it is visible to this conversation only (the user's card lists it). **Requires human approval** \u2014 the call is refused when the deployment has no approval surface. Refused: a relative path, the machine root \`/\`, a system tree, a directory that is the current workspace **or one of its ancestors** (that would hand the model the whole parent tree), and more than ${SESSION_DIRECTORY_MAX_ROOTS} directories in one conversation. Prefer asking the user to add a directory when you are unsure: this tool changes your own permission boundary.`,
|
|
14691
|
+
parameters: {
|
|
14692
|
+
path: {
|
|
14693
|
+
type: "string",
|
|
14694
|
+
description: "Absolute path. Local spelling for a local directory, absolute remote path when `connectionId` is given.",
|
|
14695
|
+
required: true
|
|
14696
|
+
},
|
|
14697
|
+
connectionId: {
|
|
14698
|
+
type: "string",
|
|
14699
|
+
description: "Configured connection id when the directory is on a remote host. Omit for a local directory."
|
|
14700
|
+
}
|
|
14701
|
+
},
|
|
14702
|
+
output: {
|
|
14703
|
+
schema: {
|
|
14704
|
+
type: "object",
|
|
14705
|
+
additionalProperties: false,
|
|
14706
|
+
properties: {
|
|
14707
|
+
path: { type: "string", required: true },
|
|
14708
|
+
added: { type: "boolean", required: true },
|
|
14709
|
+
anchorPath: { type: "string" },
|
|
14710
|
+
note: { type: "string" }
|
|
14711
|
+
}
|
|
14712
|
+
},
|
|
14713
|
+
render: (_args, value) => {
|
|
14714
|
+
const record2 = value;
|
|
14715
|
+
const anchor = typeof record2["anchorPath"] === "string" ? `\uFF08\u951A\u70B9 ${record2["anchorPath"]}\uFF09` : "";
|
|
14716
|
+
return [{ type: "text", text: `\u5DF2\u52A0\u5165\u672C\u4F1A\u8BDD\uFF1A${String(record2["path"] ?? "")}${anchor}\u3002` }];
|
|
14717
|
+
}
|
|
14718
|
+
},
|
|
14719
|
+
async execute(args, exec) {
|
|
14720
|
+
const record2 = args ?? {};
|
|
14721
|
+
const path = typeof record2["path"] === "string" ? record2["path"].trim() : "";
|
|
14722
|
+
if (path === "") throw new Error("path \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32");
|
|
14723
|
+
const sessionId = deps.sessionId();
|
|
14724
|
+
if (sessionId === void 0 || sessionId === "") throw new Error("\u62FF\u4E0D\u5230\u5F53\u524D\u4F1A\u8BDD id\uFF0C\u65E0\u6CD5\u6DFB\u52A0\u76EE\u5F55");
|
|
14725
|
+
const workspacePath = deps.workspacePathOf(sessionId);
|
|
14726
|
+
if (workspacePath === void 0 || workspacePath === "") {
|
|
14727
|
+
throw new Error("\u62FF\u4E0D\u5230\u5F53\u524D\u5DE5\u4F5C\u533A\u8DEF\u5F84\uFF0C\u65E0\u6CD5\u628A\u76EE\u5F55\u52A0\u5165\u672C\u4F1A\u8BDD");
|
|
14728
|
+
}
|
|
14729
|
+
const connectionId = typeof record2["connectionId"] === "string" && record2["connectionId"] !== "" ? record2["connectionId"] : void 0;
|
|
14730
|
+
const outcome = await deps.directories.add({
|
|
14731
|
+
location: connectionId === void 0 ? "local" : "remote",
|
|
14732
|
+
path,
|
|
14733
|
+
...connectionId === void 0 ? {} : { connectionId },
|
|
14734
|
+
workspacePath,
|
|
14735
|
+
sessionId
|
|
14736
|
+
});
|
|
14737
|
+
if (!outcome.ok) throw new Error(outcome.reason);
|
|
14738
|
+
return {
|
|
14739
|
+
path,
|
|
14740
|
+
added: true,
|
|
14741
|
+
...outcome.anchorPath === void 0 ? {} : { anchorPath: outcome.anchorPath },
|
|
14742
|
+
note: "\u5DF2\u52A0\u5165\u672C\u4F1A\u8BDD\uFF1B`workspace-write` \u4E0B\u8BE5\u76EE\u5F55\u53EF\u8BFB\u53EF\u5199\u3002"
|
|
14743
|
+
};
|
|
14744
|
+
}
|
|
14745
|
+
}),
|
|
14746
|
+
defineTool2({
|
|
14747
|
+
name: SESSION_DIRECTORY_TOOL_NAMES.remove,
|
|
14748
|
+
description: "Remove a directory that **this conversation added** from its list. The directory stays registered for any other conversation that added it; a directory the user added in the plugin settings, and the conversation workspace itself, cannot be removed here. Removing only narrows access, so it needs no approval.",
|
|
14749
|
+
parameters: {
|
|
14750
|
+
path: { type: "string", description: "Absolute path of the directory to remove, as it was added.", required: true },
|
|
14751
|
+
connectionId: {
|
|
14752
|
+
type: "string",
|
|
14753
|
+
description: "Connection id when the directory is remote; must match the one it was added with."
|
|
14754
|
+
}
|
|
14755
|
+
},
|
|
14756
|
+
output: {
|
|
14757
|
+
schema: {
|
|
14758
|
+
type: "object",
|
|
14759
|
+
additionalProperties: false,
|
|
14760
|
+
properties: {
|
|
14761
|
+
path: { type: "string", required: true },
|
|
14762
|
+
removed: { type: "boolean", required: true },
|
|
14763
|
+
note: { type: "string" }
|
|
14764
|
+
}
|
|
14765
|
+
},
|
|
14766
|
+
render: (_args, value) => {
|
|
14767
|
+
const record2 = value;
|
|
14768
|
+
return [{ type: "text", text: `\u5DF2\u4ECE\u672C\u4F1A\u8BDD\u79FB\u9664\uFF1A${String(record2["path"] ?? "")}\u3002` }];
|
|
14769
|
+
}
|
|
14770
|
+
},
|
|
14771
|
+
async execute(args) {
|
|
14772
|
+
const record2 = args ?? {};
|
|
14773
|
+
const path = typeof record2["path"] === "string" ? record2["path"].trim() : "";
|
|
14774
|
+
if (path === "") throw new Error("path \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32");
|
|
14775
|
+
const sessionId = deps.sessionId();
|
|
14776
|
+
if (sessionId === void 0 || sessionId === "") throw new Error("\u62FF\u4E0D\u5230\u5F53\u524D\u4F1A\u8BDD id\uFF0C\u65E0\u6CD5\u79FB\u9664\u76EE\u5F55");
|
|
14777
|
+
const workspacePath = deps.workspacePathOf(sessionId);
|
|
14778
|
+
if (workspacePath === void 0 || workspacePath === "") {
|
|
14779
|
+
throw new Error("\u62FF\u4E0D\u5230\u5F53\u524D\u5DE5\u4F5C\u533A\u8DEF\u5F84\uFF0C\u65E0\u6CD5\u79FB\u9664\u76EE\u5F55");
|
|
14780
|
+
}
|
|
14781
|
+
const connectionId = typeof record2["connectionId"] === "string" && record2["connectionId"] !== "" ? record2["connectionId"] : void 0;
|
|
14782
|
+
const outcome = await deps.directories.remove({
|
|
14783
|
+
path,
|
|
14784
|
+
...connectionId === void 0 ? {} : { connectionId },
|
|
14785
|
+
workspacePath,
|
|
14786
|
+
sessionId
|
|
14787
|
+
});
|
|
14788
|
+
if (!outcome.ok) throw new Error(outcome.reason);
|
|
14789
|
+
return { path, removed: true, note: "\u5DF2\u4ECE\u672C\u4F1A\u8BDD\u79FB\u9664\u3002" };
|
|
14790
|
+
}
|
|
14791
|
+
})
|
|
14792
|
+
];
|
|
14793
|
+
}
|
|
14794
|
+
function sessionDirectoryApprovalGate(ctx) {
|
|
14795
|
+
return ctx.on("tools/pre-execute", async (exec, next) => {
|
|
14796
|
+
if (exec.name !== SESSION_DIRECTORY_TOOL_NAMES.add) return next();
|
|
14797
|
+
const args = exec.arguments ?? {};
|
|
14798
|
+
const target = String(args["path"] ?? "");
|
|
14799
|
+
const where = typeof args["connectionId"] === "string" && args["connectionId"] !== "" ? `${args["connectionId"]}:${target}` : target;
|
|
14800
|
+
return {
|
|
14801
|
+
kind: "ask",
|
|
14802
|
+
reason: `${SESSION_DIRECTORY_TOOL_NAMES.add} \u60F3\u628A ${where} \u52A0\u5165\u672C\u4F1A\u8BDD\uFF08\u4F1A\u6269\u5927\u6A21\u578B\u53EF\u8BFB\u5199\u7684\u8303\u56F4\uFF09`,
|
|
14803
|
+
displayReason: {
|
|
14804
|
+
en: `Add ${where} to this conversation? It becomes readable and writable by the model.`,
|
|
14805
|
+
zh: `\u628A ${where} \u52A0\u5165\u672C\u4F1A\u8BDD\uFF1F\u52A0\u5165\u540E\u6A21\u578B\u53EF\u8BFB\u5199\u8BE5\u76EE\u5F55\u3002`
|
|
14806
|
+
}
|
|
14807
|
+
};
|
|
14808
|
+
});
|
|
14809
|
+
}
|
|
14810
|
+
function registerSessionDirectoryTools(ctx, deps) {
|
|
14811
|
+
const disposers = sessionDirectoryToolDefinitions(deps).map((definition) => ctx.tools.register(definition));
|
|
14812
|
+
const disposeGate = sessionDirectoryApprovalGate(ctx);
|
|
14813
|
+
return () => {
|
|
14814
|
+
for (const dispose of disposers.splice(0)) dispose();
|
|
14815
|
+
disposeGate();
|
|
14816
|
+
};
|
|
14817
|
+
}
|
|
14818
|
+
|
|
14819
|
+
// src/shared/routes.ts
|
|
14820
|
+
var ROOTS_API_PATH = "/api/over-ssh/roots";
|
|
14821
|
+
var SESSION_API_PATH = "/api/over-ssh/sessions";
|
|
14822
|
+
function asRouteHost(value) {
|
|
14823
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
14824
|
+
const register = value.register;
|
|
14825
|
+
return typeof register === "function" ? value : void 0;
|
|
14826
|
+
}
|
|
14827
|
+
|
|
14828
|
+
// src/host/app/roots/roots-api-contract.ts
|
|
14829
|
+
var ROOTS_API_MAX_BODY_BYTES = 64 * 1024;
|
|
14830
|
+
var DIRECTORY_LIST_MAX_ENTRIES = 2e3;
|
|
14831
|
+
var REMOTE_LIST_MAX_ENTRIES = DIRECTORY_LIST_MAX_ENTRIES;
|
|
14832
|
+
var LOCAL_LIST_MAX_ENTRIES = DIRECTORY_LIST_MAX_ENTRIES;
|
|
14833
|
+
var ROOTS_API_ACTIONS = [
|
|
14834
|
+
"add-root",
|
|
14835
|
+
"remove-root",
|
|
14836
|
+
"add-connection",
|
|
14837
|
+
"update-connection",
|
|
14838
|
+
"test-connection",
|
|
14839
|
+
"remove-connection",
|
|
14840
|
+
"list-remote",
|
|
14841
|
+
"list-local",
|
|
14842
|
+
"list-ssh-config",
|
|
14843
|
+
"import-ssh-config",
|
|
14844
|
+
"mkdir-remote",
|
|
14845
|
+
"rename-remote",
|
|
14846
|
+
"remove-remote",
|
|
14847
|
+
"list-transfers",
|
|
14848
|
+
"clear-transfers"
|
|
14849
|
+
];
|
|
14850
|
+
var ROOT_FIELDS = ["location", "path", "connectionId", "label", "writable", "workspaces", "sessions"];
|
|
14851
|
+
var CONNECTION_FIELDS = ["id", "host", "user", "port", "authKind", "keyPath", "credentialKey", "transport"];
|
|
14852
|
+
var UPDATE_CONNECTION_FIELDS = ["host", "user", "port", "authKind", "keyPath", "credentialKey", "transport"];
|
|
14853
|
+
function trustVerdict(req) {
|
|
14854
|
+
const host = headerOf(req, "host");
|
|
14855
|
+
if (host === void 0) return "no-host";
|
|
14856
|
+
const hostUrl = parseAuthority(host);
|
|
14857
|
+
if (hostUrl === void 0) return "bad-host";
|
|
14858
|
+
if (headerOf(req, "sec-fetch-site") === "cross-site") return "cross-site";
|
|
14859
|
+
const origin = headerOf(req, "origin");
|
|
14860
|
+
if (origin === void 0) return "ok";
|
|
14861
|
+
const trimmed = origin.trim();
|
|
14862
|
+
if (trimmed === "" || trimmed === "null") return "bad-origin";
|
|
14863
|
+
let originUrl;
|
|
14864
|
+
try {
|
|
14865
|
+
originUrl = new URL(trimmed);
|
|
14866
|
+
} catch {
|
|
14867
|
+
return "bad-origin";
|
|
14868
|
+
}
|
|
14869
|
+
if (originUrl.protocol !== "http:" && originUrl.protocol !== "https:") return "bad-origin";
|
|
14870
|
+
if (originUrl.host === hostUrl.host) return "ok";
|
|
14871
|
+
if (originUrl.port === "" && originUrl.hostname === hostUrl.hostname) return "ok";
|
|
14872
|
+
return "bad-origin";
|
|
14873
|
+
}
|
|
14874
|
+
function trustMessage(verdict) {
|
|
14875
|
+
switch (verdict) {
|
|
14876
|
+
case "no-host":
|
|
14877
|
+
return "\u8BF7\u6C42\u7F3A\u5C11 Host \u5934\uFF0C\u62D2\u7EDD";
|
|
14878
|
+
case "bad-host":
|
|
14879
|
+
return "Host \u5934\u65E0\u6CD5\u89E3\u6790\uFF0C\u62D2\u7EDD";
|
|
14880
|
+
case "cross-site":
|
|
14881
|
+
return "\u8DE8\u7AD9\u8BF7\u6C42\uFF08Sec-Fetch-Site: cross-site\uFF09\u88AB\u62D2\u7EDD";
|
|
14882
|
+
case "bad-origin":
|
|
14883
|
+
return "\u8BF7\u6C42\u7684 Origin \u4E0E Host \u4E0D\u540C\u6E90\uFF0C\u62D2\u7EDD";
|
|
14884
|
+
default:
|
|
14885
|
+
return "ok";
|
|
14886
|
+
}
|
|
14887
|
+
}
|
|
14888
|
+
function headerOf(req, name2) {
|
|
14889
|
+
const value = req.headers[name2] ?? req.headers[name2.toLowerCase()];
|
|
14890
|
+
if (value === void 0) return void 0;
|
|
14891
|
+
return Array.isArray(value) ? value[0] : value;
|
|
14346
14892
|
}
|
|
14347
|
-
function headerOf(req, name2) {
|
|
14348
|
-
const value = req.headers[name2] ?? req.headers[name2.toLowerCase()];
|
|
14349
|
-
if (value === void 0) return void 0;
|
|
14350
|
-
return Array.isArray(value) ? value[0] : value;
|
|
14351
|
-
}
|
|
14352
14893
|
function parseAuthority(authority) {
|
|
14353
14894
|
try {
|
|
14354
14895
|
return new URL(`http://${authority}`);
|
|
@@ -14529,30 +15070,6 @@ async function handleRootActions(action, body, req, res, deps) {
|
|
|
14529
15070
|
return false;
|
|
14530
15071
|
}
|
|
14531
15072
|
}
|
|
14532
|
-
async function refreshLiveScope(deps, key, state) {
|
|
14533
|
-
const persisted = findRootInState(state, key);
|
|
14534
|
-
if (persisted === void 0) return;
|
|
14535
|
-
const raw = await Promise.resolve(deps.listRoots());
|
|
14536
|
-
const liveEntry = raw.find((candidate) => liveKeyOfRecord(candidate, persisted) !== void 0);
|
|
14537
|
-
if (liveEntry === void 0) return;
|
|
14538
|
-
if (liveScopeIsCurrent(liveEntry, persisted)) return;
|
|
14539
|
-
const liveKey = liveKeyOfRecord(liveEntry, persisted);
|
|
14540
|
-
if (liveKey === void 0) return;
|
|
14541
|
-
await deps.unregisterRoot(liveKey);
|
|
14542
|
-
await deps.registerRoot(persisted);
|
|
14543
|
-
}
|
|
14544
|
-
function liveScopeIsCurrent(raw, entry) {
|
|
14545
|
-
const outer = asRecord2(raw) ?? {};
|
|
14546
|
-
const source = asRecord2(outer["root"]) ?? outer;
|
|
14547
|
-
if (source["shared"] === true) return true;
|
|
14548
|
-
return sameStrings(source["workspaces"], entry.workspaces) && sameStrings(source["sessions"], entry.sessions);
|
|
14549
|
-
}
|
|
14550
|
-
function sameStrings(left, right) {
|
|
14551
|
-
const asSet = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string").sort() : [];
|
|
14552
|
-
const a = asSet(left);
|
|
14553
|
-
const b = asSet(right);
|
|
14554
|
-
return a.length === b.length && a.every((value, index) => value === b[index]);
|
|
14555
|
-
}
|
|
14556
15073
|
function rootAddedBySession(state, key, sessionId) {
|
|
14557
15074
|
const entry = findRootInState(state, key);
|
|
14558
15075
|
return (entry?.sessions ?? []).includes(sessionId);
|
|
@@ -14607,129 +15124,6 @@ function optionalWorkspaceName(value) {
|
|
|
14607
15124
|
// src/host/app/roots/roots-api-connections.ts
|
|
14608
15125
|
init_errors();
|
|
14609
15126
|
import { homedir as homedir3 } from "node:os";
|
|
14610
|
-
|
|
14611
|
-
// src/host/domain/roots/ssh-config.ts
|
|
14612
|
-
function stripComment(line) {
|
|
14613
|
-
let inQuotes = false;
|
|
14614
|
-
for (let index = 0; index < line.length; index++) {
|
|
14615
|
-
const char = line[index];
|
|
14616
|
-
if (char === '"') inQuotes = !inQuotes;
|
|
14617
|
-
else if (char === "#" && !inQuotes) return line.slice(0, index);
|
|
14618
|
-
}
|
|
14619
|
-
return line;
|
|
14620
|
-
}
|
|
14621
|
-
function unquote(value) {
|
|
14622
|
-
if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) return value.slice(1, -1);
|
|
14623
|
-
return value;
|
|
14624
|
-
}
|
|
14625
|
-
function splitKeyValue(line) {
|
|
14626
|
-
const match = /^([A-Za-z][A-Za-z0-9]*)\s*(?:=|\s)\s*(.*)$/.exec(line);
|
|
14627
|
-
if (match === null) return void 0;
|
|
14628
|
-
const key = match[1];
|
|
14629
|
-
const value = match[2];
|
|
14630
|
-
if (key === void 0 || value === void 0) return void 0;
|
|
14631
|
-
return [key.toLowerCase(), unquote(value.trim())];
|
|
14632
|
-
}
|
|
14633
|
-
function parseSshConfig(text8) {
|
|
14634
|
-
const entries = [];
|
|
14635
|
-
let current;
|
|
14636
|
-
const lines = text8.split(/\r?\n/);
|
|
14637
|
-
for (let index = 0; index < lines.length; index++) {
|
|
14638
|
-
const raw = lines[index] ?? "";
|
|
14639
|
-
const line = stripComment(raw).trim();
|
|
14640
|
-
if (line === "") continue;
|
|
14641
|
-
const pair = splitKeyValue(line);
|
|
14642
|
-
if (pair === void 0) continue;
|
|
14643
|
-
const [key, value] = pair;
|
|
14644
|
-
if (key === "host") {
|
|
14645
|
-
const patterns = value.split(/\s+/).filter((part) => part !== "");
|
|
14646
|
-
if (patterns.length === 0) continue;
|
|
14647
|
-
current = { patterns, identityFiles: [], includes: [], line: index + 1 };
|
|
14648
|
-
entries.push(current);
|
|
14649
|
-
continue;
|
|
14650
|
-
}
|
|
14651
|
-
if (current === void 0) continue;
|
|
14652
|
-
switch (key) {
|
|
14653
|
-
case "hostname":
|
|
14654
|
-
current.hostName ??= value;
|
|
14655
|
-
break;
|
|
14656
|
-
case "user":
|
|
14657
|
-
current.user ??= value;
|
|
14658
|
-
break;
|
|
14659
|
-
case "port": {
|
|
14660
|
-
if (current.port !== void 0) break;
|
|
14661
|
-
const port = Number.parseInt(value, 10);
|
|
14662
|
-
if (Number.isInteger(port) && port > 0 && port <= 65535) current.port = port;
|
|
14663
|
-
break;
|
|
14664
|
-
}
|
|
14665
|
-
case "identityfile":
|
|
14666
|
-
current.identityFiles.push(value);
|
|
14667
|
-
break;
|
|
14668
|
-
case "proxyjump":
|
|
14669
|
-
current.proxyJump ??= value.toLowerCase() === "none" ? "" : value;
|
|
14670
|
-
break;
|
|
14671
|
-
case "include":
|
|
14672
|
-
current.includes.push(value);
|
|
14673
|
-
break;
|
|
14674
|
-
default:
|
|
14675
|
-
break;
|
|
14676
|
-
}
|
|
14677
|
-
}
|
|
14678
|
-
return entries.map((entry) => ({
|
|
14679
|
-
patterns: entry.patterns,
|
|
14680
|
-
...entry.hostName === void 0 ? {} : { hostName: entry.hostName },
|
|
14681
|
-
...entry.user === void 0 ? {} : { user: entry.user },
|
|
14682
|
-
...entry.port === void 0 ? {} : { port: entry.port },
|
|
14683
|
-
identityFiles: entry.identityFiles,
|
|
14684
|
-
...entry.proxyJump === void 0 || entry.proxyJump === "" ? {} : { proxyJump: entry.proxyJump },
|
|
14685
|
-
includes: entry.includes,
|
|
14686
|
-
line: entry.line
|
|
14687
|
-
}));
|
|
14688
|
-
}
|
|
14689
|
-
function isConcretePattern(pattern) {
|
|
14690
|
-
return !/[*?!]/.test(pattern);
|
|
14691
|
-
}
|
|
14692
|
-
function expandHome2(path, home) {
|
|
14693
|
-
if (path === "~") return home;
|
|
14694
|
-
if (path.startsWith("~/") || path.startsWith("~\\")) return `${home.replace(/[\\/]+$/, "")}${path.slice(1)}`;
|
|
14695
|
-
return path;
|
|
14696
|
-
}
|
|
14697
|
-
function importableEntries(entries, options = {}) {
|
|
14698
|
-
const home = options.home ?? "";
|
|
14699
|
-
const excluded = new Set(options.excluded ?? []);
|
|
14700
|
-
const seen = /* @__PURE__ */ new Set();
|
|
14701
|
-
const drafts = [];
|
|
14702
|
-
const skipped = [];
|
|
14703
|
-
for (const entry of entries) {
|
|
14704
|
-
for (const pattern of entry.patterns) {
|
|
14705
|
-
if (!isConcretePattern(pattern)) {
|
|
14706
|
-
skipped.push({ pattern, reason: "wildcard", line: entry.line });
|
|
14707
|
-
continue;
|
|
14708
|
-
}
|
|
14709
|
-
if (seen.has(pattern)) {
|
|
14710
|
-
skipped.push({ pattern, reason: "duplicate", line: entry.line });
|
|
14711
|
-
continue;
|
|
14712
|
-
}
|
|
14713
|
-
if (excluded.has(pattern)) {
|
|
14714
|
-
skipped.push({ pattern, reason: "already-imported", line: entry.line });
|
|
14715
|
-
continue;
|
|
14716
|
-
}
|
|
14717
|
-
seen.add(pattern);
|
|
14718
|
-
const identityFile = entry.identityFiles[0];
|
|
14719
|
-
drafts.push({
|
|
14720
|
-
alias: pattern,
|
|
14721
|
-
hostName: entry.hostName ?? pattern,
|
|
14722
|
-
...entry.user === void 0 ? {} : { user: entry.user },
|
|
14723
|
-
...entry.port === void 0 ? {} : { port: entry.port },
|
|
14724
|
-
...identityFile === void 0 ? {} : { identityFile: home === "" ? identityFile : expandHome2(identityFile, home) },
|
|
14725
|
-
...entry.proxyJump === void 0 ? {} : { proxyJump: entry.proxyJump }
|
|
14726
|
-
});
|
|
14727
|
-
}
|
|
14728
|
-
}
|
|
14729
|
-
return { drafts, skipped };
|
|
14730
|
-
}
|
|
14731
|
-
|
|
14732
|
-
// src/host/app/roots/roots-api-connections.ts
|
|
14733
15127
|
async function handleConnectionActions(action, body, req, res, deps) {
|
|
14734
15128
|
switch (action) {
|
|
14735
15129
|
case "list-ssh-config": {
|
|
@@ -14748,14 +15142,12 @@ async function handleConnectionActions(action, body, req, res, deps) {
|
|
|
14748
15142
|
}
|
|
14749
15143
|
if (wanted.length === 0) throw new SshDevError("CONNECTION_INVALID", "\u6CA1\u6709\u9009\u62E9\u8981\u5BFC\u5165\u7684\u522B\u540D", {});
|
|
14750
15144
|
const before = await currentUserState(deps);
|
|
14751
|
-
const
|
|
14752
|
-
if (
|
|
15145
|
+
const plan = await planSshImport(deps, before.connections.map((entry) => entry.id));
|
|
15146
|
+
if (plan === void 0) {
|
|
14753
15147
|
throw new SshDevError("CONNECTION_INVALID", "\u8FD9\u4E2A\u5BBF\u4E3B\u7EC4\u5408\u6CA1\u6709\u63D0\u4F9B ~/.ssh/config \u7684\u8BFB\u53D6\u80FD\u529B", {});
|
|
14754
15148
|
}
|
|
14755
|
-
const byAlias = new Map(
|
|
14756
|
-
const
|
|
14757
|
-
const drafts = importableEntries(parseSshConfig(text8 ?? ""), { home: homedir3() }).drafts;
|
|
14758
|
-
const draftByAlias = new Map(drafts.map((draft) => [draft.alias, draft]));
|
|
15149
|
+
const byAlias = new Map(plan.candidates.map((candidate) => [candidate.alias, candidate]));
|
|
15150
|
+
const draftByAlias = new Map(plan.drafts.map((draft) => [draft.alias, draft]));
|
|
14759
15151
|
const accepted = [];
|
|
14760
15152
|
const refused = [];
|
|
14761
15153
|
const seenIds = new Set(before.connections.map((entry) => entry.id));
|
|
@@ -14773,7 +15165,10 @@ async function handleConnectionActions(action, body, req, res, deps) {
|
|
|
14773
15165
|
seenIds.add(candidate.suggestedId);
|
|
14774
15166
|
accepted.push(connectionRecordFromCandidate(draft, candidate.suggestedId));
|
|
14775
15167
|
}
|
|
14776
|
-
|
|
15168
|
+
if (accepted.length > 0) {
|
|
15169
|
+
if (deps.addConnections !== void 0) await deps.addConnections(accepted);
|
|
15170
|
+
else for (const record2 of accepted) await deps.addConnection(record2);
|
|
15171
|
+
}
|
|
14777
15172
|
const state = await reconcile(deps, (current) => {
|
|
14778
15173
|
let next = current;
|
|
14779
15174
|
for (const record2 of accepted) {
|
|
@@ -15015,11 +15410,11 @@ function publicProbeResult(raw) {
|
|
|
15015
15410
|
ms
|
|
15016
15411
|
};
|
|
15017
15412
|
}
|
|
15018
|
-
async function
|
|
15413
|
+
async function planSshImport(deps, excluded) {
|
|
15019
15414
|
const read = deps.readSshConfig;
|
|
15020
15415
|
if (read === void 0) return void 0;
|
|
15021
15416
|
const text8 = await read();
|
|
15022
|
-
if (text8 === void 0 || text8.trim() === "") return { candidates: [], skipped: [] };
|
|
15417
|
+
if (text8 === void 0 || text8.trim() === "") return { candidates: [], skipped: [], drafts: [] };
|
|
15023
15418
|
const result = importableEntries(parseSshConfig(text8), { home: homedir3(), excluded });
|
|
15024
15419
|
const taken = new Set(excluded);
|
|
15025
15420
|
const candidates = [];
|
|
@@ -15045,7 +15440,12 @@ async function sshConfigCandidates(deps, excluded) {
|
|
|
15045
15440
|
suggestedId
|
|
15046
15441
|
});
|
|
15047
15442
|
}
|
|
15048
|
-
return { candidates, skipped };
|
|
15443
|
+
return { candidates, skipped, drafts: result.drafts };
|
|
15444
|
+
}
|
|
15445
|
+
async function sshConfigCandidates(deps, excluded) {
|
|
15446
|
+
const plan = await planSshImport(deps, excluded);
|
|
15447
|
+
if (plan === void 0) return void 0;
|
|
15448
|
+
return { candidates: plan.candidates, skipped: plan.skipped };
|
|
15049
15449
|
}
|
|
15050
15450
|
function connectionRecordFromCandidate(draft, id) {
|
|
15051
15451
|
const keyPath = draft.identityFile === void 0 || draft.identityFile === "" ? void 0 : draft.identityFile;
|
|
@@ -15537,6 +15937,12 @@ function createRootsWiring(deps) {
|
|
|
15537
15937
|
const transfers = deps.transfers;
|
|
15538
15938
|
const readCredentials = deps.readCredentials;
|
|
15539
15939
|
const testConnection = deps.testConnection;
|
|
15940
|
+
const sessionDirectories = createSessionDirectories({
|
|
15941
|
+
mutateUserState: (transition) => store2.mutate(transition),
|
|
15942
|
+
listRoots: () => deps.listRoots(),
|
|
15943
|
+
registerRoot: deps.registerRoot,
|
|
15944
|
+
unregisterRoot: deps.unregisterRoot
|
|
15945
|
+
});
|
|
15540
15946
|
const apiDeps = {
|
|
15541
15947
|
listRoots: () => deps.listRoots(),
|
|
15542
15948
|
listConnections: () => deps.listConnections(),
|
|
@@ -15553,6 +15959,39 @@ function createRootsWiring(deps) {
|
|
|
15553
15959
|
deps.applyConnection(entry, entry.id);
|
|
15554
15960
|
return next;
|
|
15555
15961
|
},
|
|
15962
|
+
addConnections: async (entries) => {
|
|
15963
|
+
if (entries.length === 0) return store2.load().then((loaded) => loaded.state);
|
|
15964
|
+
const applied = [];
|
|
15965
|
+
try {
|
|
15966
|
+
for (const entry of entries) {
|
|
15967
|
+
deps.applyConnection(entry, entry.id);
|
|
15968
|
+
applied.push(entry.id);
|
|
15969
|
+
}
|
|
15970
|
+
} catch (error) {
|
|
15971
|
+
for (const id of applied) {
|
|
15972
|
+
try {
|
|
15973
|
+
deps.removeConnection(id);
|
|
15974
|
+
} catch {
|
|
15975
|
+
}
|
|
15976
|
+
}
|
|
15977
|
+
throw error;
|
|
15978
|
+
}
|
|
15979
|
+
try {
|
|
15980
|
+
return await store2.mutate((state) => {
|
|
15981
|
+
let next = state;
|
|
15982
|
+
for (const entry of entries) next = stateHasConnection(next, entry.id) ? next : addConnectionToState(next, entry);
|
|
15983
|
+
return next;
|
|
15984
|
+
});
|
|
15985
|
+
} catch (error) {
|
|
15986
|
+
for (const id of applied) {
|
|
15987
|
+
try {
|
|
15988
|
+
deps.removeConnection(id);
|
|
15989
|
+
} catch {
|
|
15990
|
+
}
|
|
15991
|
+
}
|
|
15992
|
+
throw error;
|
|
15993
|
+
}
|
|
15994
|
+
},
|
|
15556
15995
|
applyConnection: async (entry, previousId) => {
|
|
15557
15996
|
deps.applyConnection(entry, previousId);
|
|
15558
15997
|
},
|
|
@@ -15636,6 +16075,7 @@ function createRootsWiring(deps) {
|
|
|
15636
16075
|
return {
|
|
15637
16076
|
api,
|
|
15638
16077
|
store: store2,
|
|
16078
|
+
sessionDirectories,
|
|
15639
16079
|
mergeUserRoots,
|
|
15640
16080
|
routeWarnings: () => [...routeWarnings],
|
|
15641
16081
|
routeFacts: () => ({ injectRan, routeRegistered, warnings: [...routeWarnings] }),
|
|
@@ -15778,13 +16218,34 @@ function createSessionApi(deps) {
|
|
|
15778
16218
|
return { handle, dispose: () => {
|
|
15779
16219
|
} };
|
|
15780
16220
|
}
|
|
16221
|
+
function abortOnDisconnect(res) {
|
|
16222
|
+
const controller = new AbortController();
|
|
16223
|
+
const closable = res;
|
|
16224
|
+
const onClose = () => {
|
|
16225
|
+
if (closable.writableEnded !== true) controller.abort();
|
|
16226
|
+
};
|
|
16227
|
+
if (typeof closable.on === "function") closable.on("close", onClose);
|
|
16228
|
+
return {
|
|
16229
|
+
signal: controller.signal,
|
|
16230
|
+
dispose: () => {
|
|
16231
|
+
if (typeof closable.off === "function") closable.off("close", onClose);
|
|
16232
|
+
}
|
|
16233
|
+
};
|
|
16234
|
+
}
|
|
15781
16235
|
async function serveSnapshot2(req, res, deps) {
|
|
15782
16236
|
const service = deps.service();
|
|
15783
16237
|
if (service === void 0) {
|
|
15784
16238
|
send(req, res, 200, { ok: true, available: false, sessions: [], bin: [], policy: deps.policy() });
|
|
15785
16239
|
return;
|
|
15786
16240
|
}
|
|
15787
|
-
const
|
|
16241
|
+
const connection = abortOnDisconnect(res);
|
|
16242
|
+
let sessions;
|
|
16243
|
+
let bin;
|
|
16244
|
+
try {
|
|
16245
|
+
[sessions, bin] = await Promise.all([service.inventory({ signal: connection.signal }), service.list()]);
|
|
16246
|
+
} finally {
|
|
16247
|
+
connection.dispose();
|
|
16248
|
+
}
|
|
15788
16249
|
send(req, res, 200, {
|
|
15789
16250
|
ok: true,
|
|
15790
16251
|
available: true,
|
|
@@ -15801,10 +16262,19 @@ async function serveAction2(req, res, deps) {
|
|
|
15801
16262
|
return;
|
|
15802
16263
|
}
|
|
15803
16264
|
const service = requireService(deps);
|
|
16265
|
+
const connection = abortOnDisconnect(res);
|
|
16266
|
+
try {
|
|
16267
|
+
return await serveActionWithSignal(action, body, req, res, deps, service, connection.signal);
|
|
16268
|
+
} finally {
|
|
16269
|
+
connection.dispose();
|
|
16270
|
+
}
|
|
16271
|
+
}
|
|
16272
|
+
async function serveActionWithSignal(action, body, req, res, deps, service, signal) {
|
|
16273
|
+
const options = { signal };
|
|
15804
16274
|
switch (action) {
|
|
15805
16275
|
case "preview": {
|
|
15806
16276
|
const ids = readIds(body);
|
|
15807
|
-
const decision = await service.previewDelete(ids);
|
|
16277
|
+
const decision = await service.previewDelete(ids, options);
|
|
15808
16278
|
send(req, res, 200, { ok: true, delete: decision.delete, refusals: decision.refusals, bytes: decision.bytes });
|
|
15809
16279
|
return;
|
|
15810
16280
|
}
|
|
@@ -15819,7 +16289,7 @@ async function serveAction2(req, res, deps) {
|
|
|
15819
16289
|
});
|
|
15820
16290
|
return;
|
|
15821
16291
|
}
|
|
15822
|
-
const result = await service.deleteSession(ids, { mode });
|
|
16292
|
+
const result = await service.deleteSession(ids, { mode, ...options });
|
|
15823
16293
|
send(req, res, 200, {
|
|
15824
16294
|
ok: true,
|
|
15825
16295
|
mode: result.mode,
|
|
@@ -15847,7 +16317,7 @@ async function serveAction2(req, res, deps) {
|
|
|
15847
16317
|
});
|
|
15848
16318
|
return;
|
|
15849
16319
|
}
|
|
15850
|
-
const result = await service.deleteWorkspaceSessions(cwd, { mode });
|
|
16320
|
+
const result = await service.deleteWorkspaceSessions(cwd, { mode, ...options });
|
|
15851
16321
|
send(req, res, 200, {
|
|
15852
16322
|
ok: true,
|
|
15853
16323
|
mode: result.mode,
|
|
@@ -15867,7 +16337,7 @@ async function serveAction2(req, res, deps) {
|
|
|
15867
16337
|
send(req, res, 400, { ok: false, error: "entryId \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32" });
|
|
15868
16338
|
return;
|
|
15869
16339
|
}
|
|
15870
|
-
const result = await service.restore(entryId);
|
|
16340
|
+
const result = await service.restore(entryId, options);
|
|
15871
16341
|
send(req, res, 200, { ok: true, restoredPath: result.restoredPath, movedBy: result.movedBy });
|
|
15872
16342
|
return;
|
|
15873
16343
|
}
|
|
@@ -15881,12 +16351,20 @@ async function serveAction2(req, res, deps) {
|
|
|
15881
16351
|
send(req, res, 400, { ok: false, error: "purge \u9700\u8981 confirm: true\uFF08\u4E0D\u53EF\u6062\u590D\uFF09", reason: "confirmation-required" });
|
|
15882
16352
|
return;
|
|
15883
16353
|
}
|
|
15884
|
-
const result = await service.purge(entryId);
|
|
16354
|
+
const result = await service.purge(entryId, options);
|
|
15885
16355
|
send(req, res, 200, { ok: true, purged: result.purged, indexStale: result.indexStale });
|
|
15886
16356
|
return;
|
|
15887
16357
|
}
|
|
15888
16358
|
case "sweep": {
|
|
15889
|
-
|
|
16359
|
+
if (body["confirm"] !== true) {
|
|
16360
|
+
send(req, res, 400, {
|
|
16361
|
+
ok: false,
|
|
16362
|
+
error: "sweep \u9700\u8981 confirm: true\uFF08\u4E0D\u53EF\u6062\u590D\uFF09",
|
|
16363
|
+
reason: "confirmation-required"
|
|
16364
|
+
});
|
|
16365
|
+
return;
|
|
16366
|
+
}
|
|
16367
|
+
const result = await service.sweep(deps.policy(), options);
|
|
15890
16368
|
send(req, res, 200, {
|
|
15891
16369
|
ok: true,
|
|
15892
16370
|
purged: result.purged.map((item) => ({ entryId: String(item.id), sessionId: item.sessionId, indexStale: item.indexStale })),
|
|
@@ -16008,10 +16486,15 @@ function rootLifecycleSeams(env, hooks) {
|
|
|
16008
16486
|
const removed = registry.removeByLooseKey(key);
|
|
16009
16487
|
binRootResolver.forget(removed ?? key);
|
|
16010
16488
|
sessionRoots.invalidate();
|
|
16011
|
-
if (anchor !== void 0
|
|
16489
|
+
if (anchor !== void 0 && !anchorStillUsedBy(registry.records, anchor)) {
|
|
16490
|
+
void rmdir(anchor).catch(() => void 0);
|
|
16491
|
+
}
|
|
16012
16492
|
}
|
|
16013
16493
|
};
|
|
16014
16494
|
}
|
|
16495
|
+
function anchorStillUsedBy(records, anchor) {
|
|
16496
|
+
return records.some((record2) => record2.root.anchorPath === anchor);
|
|
16497
|
+
}
|
|
16015
16498
|
function anchorPathOfRecord(records, key) {
|
|
16016
16499
|
const target = key.toLowerCase();
|
|
16017
16500
|
for (const record2 of records) {
|
|
@@ -16030,18 +16513,18 @@ function connectionLifecycleSeams(env, hooks) {
|
|
|
16030
16513
|
// Keeping them across an edit answered for the previous host (audit finding: changing a connection's
|
|
16031
16514
|
// host left its remote roots connected to the old machine); keeping them across a rename answered under
|
|
16032
16515
|
// the new name for the old host.
|
|
16033
|
-
applyConnection: (entry, previousId) => {
|
|
16516
|
+
applyConnection: async (entry, previousId) => {
|
|
16034
16517
|
const profile = validateConnectionProfile(toProfileInput({ ...entry, transport: "agent" }));
|
|
16035
16518
|
profiles.set(profile.id, profile);
|
|
16036
16519
|
if (previousId !== profile.id) {
|
|
16037
16520
|
profiles.delete(previousId);
|
|
16038
|
-
hooks.forgetPerConnection(previousId);
|
|
16521
|
+
await hooks.forgetPerConnection(previousId);
|
|
16039
16522
|
indexes.delete(previousId);
|
|
16040
16523
|
retireBrowseBackend(browseBackends, previousId);
|
|
16041
16524
|
forgetBackendsOf(previousId);
|
|
16042
16525
|
void transports.release(previousId).catch(() => void 0);
|
|
16043
16526
|
}
|
|
16044
|
-
hooks.forgetPerConnection(profile.id);
|
|
16527
|
+
await hooks.forgetPerConnection(profile.id);
|
|
16045
16528
|
askpass.revive(profile.id);
|
|
16046
16529
|
indexes.delete(profile.id);
|
|
16047
16530
|
retireBrowseBackend(browseBackends, profile.id);
|
|
@@ -16213,11 +16696,17 @@ function provideHostServices(env) {
|
|
|
16213
16696
|
}
|
|
16214
16697
|
function provideAgentStatus(env) {
|
|
16215
16698
|
const { ctx, agentBridge, installations, askpass, remoteHome } = env;
|
|
16216
|
-
const
|
|
16699
|
+
const forgetPerConnectionAsync = async (connectionId) => {
|
|
16217
16700
|
agentBridge.forget(connectionId);
|
|
16218
16701
|
installations.forget(connectionId);
|
|
16219
|
-
void askpass.forget(connectionId);
|
|
16220
16702
|
remoteHome.forget(connectionId);
|
|
16703
|
+
try {
|
|
16704
|
+
await askpass.forget(connectionId);
|
|
16705
|
+
} catch {
|
|
16706
|
+
}
|
|
16707
|
+
};
|
|
16708
|
+
const forgetPerConnection = (connectionId) => {
|
|
16709
|
+
void forgetPerConnectionAsync(connectionId);
|
|
16221
16710
|
};
|
|
16222
16711
|
const disposeAgent = ctx.provide("sshDevAgent", {
|
|
16223
16712
|
outcome: (connectionId) => agentBridge.installationFor(connectionId),
|
|
@@ -16225,7 +16714,7 @@ function provideAgentStatus(env) {
|
|
|
16225
16714
|
forgetPerConnection(connectionId);
|
|
16226
16715
|
}
|
|
16227
16716
|
});
|
|
16228
|
-
return { forgetPerConnection, disposeAgent };
|
|
16717
|
+
return { forgetPerConnection, forgetPerConnectionAsync, disposeAgent };
|
|
16229
16718
|
}
|
|
16230
16719
|
function createBinHousekeeping(env) {
|
|
16231
16720
|
const { ctx, limits, binService } = env;
|
|
@@ -16292,7 +16781,7 @@ function mergePersistedRoots(env) {
|
|
|
16292
16781
|
});
|
|
16293
16782
|
}
|
|
16294
16783
|
function createTeardown(env) {
|
|
16295
|
-
const { activationTimers, routes, hostServices, disposeAgent, tools } = env;
|
|
16784
|
+
const { activationTimers, routes, hostServices, disposeAgent, tools, disposeSessionDirectories } = env;
|
|
16296
16785
|
return async () => {
|
|
16297
16786
|
for (const timer of activationTimers) clearTimeout(timer);
|
|
16298
16787
|
routes.rootsWiring.dispose();
|
|
@@ -16305,6 +16794,7 @@ function createTeardown(env) {
|
|
|
16305
16794
|
await tools.disposeLsp();
|
|
16306
16795
|
hostServices.disposeBin();
|
|
16307
16796
|
hostServices.disposeSessionTools();
|
|
16797
|
+
disposeSessionDirectories();
|
|
16308
16798
|
hostServices.disposeSessionRoots();
|
|
16309
16799
|
hostServices.disposeRange();
|
|
16310
16800
|
hostServices.disposeTransfer();
|
|
@@ -16335,7 +16825,7 @@ function wireFs(env) {
|
|
|
16335
16825
|
sessionRoots: env.sessionRoots,
|
|
16336
16826
|
readBytesRange: env.readBytesRange
|
|
16337
16827
|
});
|
|
16338
|
-
const {
|
|
16828
|
+
const { forgetPerConnectionAsync, disposeAgent } = provideAgentStatus({
|
|
16339
16829
|
ctx,
|
|
16340
16830
|
agentBridge: env.agentBridge,
|
|
16341
16831
|
installations: env.installations,
|
|
@@ -16345,14 +16835,19 @@ function wireFs(env) {
|
|
|
16345
16835
|
const runBinHousekeeping = createBinHousekeeping({ ctx, limits, binService: env.binService });
|
|
16346
16836
|
registerConfiguredRoots({ ctx, config, anchorRootDir, registry, prepareBinRoots, runBinHousekeeping });
|
|
16347
16837
|
const routes = registerPanelRoutes(env, {
|
|
16348
|
-
forgetPerConnection,
|
|
16838
|
+
forgetPerConnection: forgetPerConnectionAsync,
|
|
16349
16839
|
runBinHousekeeping,
|
|
16350
16840
|
warn: (message) => ctx.logger?.warn?.(`over-ssh: ${message}`)
|
|
16351
16841
|
});
|
|
16842
|
+
const disposeSessionDirectories = registerSessionDirectoryTools(ctx, {
|
|
16843
|
+
directories: routes.rootsWiring.sessionDirectories,
|
|
16844
|
+
sessionId: () => currentSessionId(),
|
|
16845
|
+
workspacePathOf: (sessionId) => readSessionCwd(ctx, sessionId)
|
|
16846
|
+
});
|
|
16352
16847
|
const activationTimers = scheduleActivationDiagnostics({ ctx, ...routes });
|
|
16353
16848
|
watchSessionRemovals({ ctx, rootsWiring: routes.rootsWiring, onSessionRemoved: env.onSessionRemoved });
|
|
16354
16849
|
mergePersistedRoots({ ctx, rootsWiring: routes.rootsWiring });
|
|
16355
|
-
return createTeardown({ activationTimers, routes, hostServices, disposeAgent, tools: env });
|
|
16850
|
+
return createTeardown({ activationTimers, routes, hostServices, disposeAgent, tools: env, disposeSessionDirectories });
|
|
16356
16851
|
});
|
|
16357
16852
|
}
|
|
16358
16853
|
|
|
@@ -16645,6 +17140,7 @@ function trimSlashes(value) {
|
|
|
16645
17140
|
// src/host/integration/roots/bin-root-resolver.ts
|
|
16646
17141
|
function createBinRootResolver(deps) {
|
|
16647
17142
|
const choices = /* @__PURE__ */ new Map();
|
|
17143
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
16648
17144
|
const fallback = (root, note) => ({
|
|
16649
17145
|
binRoot: binRootInsideRoot(root, deps.binDir),
|
|
16650
17146
|
placement: "inside",
|
|
@@ -16665,6 +17161,17 @@ function createBinRootResolver(deps) {
|
|
|
16665
17161
|
const key = rootKey(root);
|
|
16666
17162
|
const existing = choices.get(key);
|
|
16667
17163
|
if (existing !== void 0) return existing;
|
|
17164
|
+
const running = inFlight.get(key);
|
|
17165
|
+
if (running !== void 0) return running;
|
|
17166
|
+
const attempt = resolveBinRoot(root);
|
|
17167
|
+
inFlight.set(key, attempt);
|
|
17168
|
+
try {
|
|
17169
|
+
return await attempt;
|
|
17170
|
+
} finally {
|
|
17171
|
+
if (inFlight.get(key) === attempt) inFlight.delete(key);
|
|
17172
|
+
}
|
|
17173
|
+
};
|
|
17174
|
+
const resolveBinRoot = async (root) => {
|
|
16668
17175
|
const beside = binRootBesideRoot(root, deps.binDir);
|
|
16669
17176
|
const parent = binParentDirectory(root);
|
|
16670
17177
|
if (beside === void 0 || parent === void 0) {
|
|
@@ -17238,7 +17745,7 @@ function execWriteRootsFor(input) {
|
|
|
17238
17745
|
}
|
|
17239
17746
|
|
|
17240
17747
|
// src/host/integration/tools/remote-tools.ts
|
|
17241
|
-
import { defineTool as
|
|
17748
|
+
import { defineTool as defineTool3 } from "@deepseek-ai/dsh-tools";
|
|
17242
17749
|
|
|
17243
17750
|
// src/host/integration/tools/session-target.ts
|
|
17244
17751
|
var NO_SESSION_CWD = "\u65E0\u6CD5\u786E\u5B9A\u8C03\u7528\u65B9\u7684\u8FDC\u7AEF\u6839\uFF1A\u8BE5\u5DE5\u5177\u9700\u8981 agent \u7684\u4F1A\u8BDD\u5DE5\u4F5C\u76EE\u5F55";
|
|
@@ -17283,7 +17790,7 @@ var READONLY_CONCURRENCY_NOTE = " Read-only: calls to this tool may overlap with
|
|
|
17283
17790
|
var EXEC_SERIAL_NOTE = " This tool is deliberately NOT concurrency-safe: remote commands run one at a time per session, so `Promise.all` over several remote_exec calls produces no overlap. That protects a shared build server from concurrent `make` runs; use one command (`a && b`) or a background job instead of relying on parallelism.";
|
|
17284
17791
|
function remoteToolDefinitions(backend) {
|
|
17285
17792
|
return [
|
|
17286
|
-
|
|
17793
|
+
defineTool3({
|
|
17287
17794
|
name: "remote_grep",
|
|
17288
17795
|
description: "Search file contents inside the remote workspace this session is working in. Returns matching lines with their file and 1-based line number. Case-insensitive and ignore-aware by default; `caseSensitive` and `noIgnore` change that. Bounded: at most 200 matches, and the result says so when the cap was reached. When the agent answers, `noIgnore` also stops `.gitignore` from hiding files; on the plain-`grep` fallback tier VCS ignores were never applied, so there the flag only controls the built-in noise directories. The result names the tier that answered in `strategy`." + READONLY_CONCURRENCY_NOTE,
|
|
17289
17796
|
isConcurrencySafe: () => true,
|
|
@@ -17356,7 +17863,7 @@ function remoteToolDefinitions(backend) {
|
|
|
17356
17863
|
};
|
|
17357
17864
|
}
|
|
17358
17865
|
}),
|
|
17359
|
-
|
|
17866
|
+
defineTool3({
|
|
17360
17867
|
name: "remote_glob",
|
|
17361
17868
|
description: "List files in the remote workspace this session is working in by **glob pattern**, matched the same way on every transport. A pattern without `/` matches the file name at any depth (`*.h` finds `app/include/header.h`); a pattern with `/` is anchored at the directory you pass in and `*` never crosses a `/`. `**` crosses zero or more segments, `?` is one character, `[abc]` and `[!abc]` are character classes, `{a,b}` alternates. Matching is case-sensitive. Paths are returned relative to the enumerated directory. An invalid pattern (an unclosed `[` or `{`) is rejected rather than answered with an empty list. Bounded: at most 500 paths, and the result says so when the cap was reached." + READONLY_CONCURRENCY_NOTE,
|
|
17362
17869
|
isConcurrencySafe: () => true,
|
|
@@ -17403,7 +17910,7 @@ function remoteToolDefinitions(backend) {
|
|
|
17403
17910
|
};
|
|
17404
17911
|
}
|
|
17405
17912
|
}),
|
|
17406
|
-
|
|
17913
|
+
defineTool3({
|
|
17407
17914
|
name: "remote_exec",
|
|
17408
17915
|
description: "Run one shell command on the remote host in the workspace this session is working in, and return its exit code, stdout and stderr. **A shell command is not confined by \u300C\u6DFB\u52A0\u76EE\u5F55\u300D**: it runs with the SSH user's own permissions, so it can read and write anything that user can, not only the directories added to this conversation. (A deployment that sets `limits.execWriteFence` puts a Landlock write fence around the workspace root and the directories this session added; the tool report says when that fence is active.) Output is bounded to 64 KiB per stream; when a stream is cut the result names that stream and, if the agent spilled it, the remote path holding the full text. Use this for remote builds, tests and version checks. Each call is a NEW shell: exported variables and `cd` do not carry over to the next call \u2014 pass `env` or put `source ... && cmd` in one command." + EXEC_SERIAL_NOTE,
|
|
17409
17916
|
parameters: {
|
|
@@ -17725,7 +18232,7 @@ function strictArgumentsGate(ctx) {
|
|
|
17725
18232
|
|
|
17726
18233
|
// src/host/integration/tools/remote-job-tools.ts
|
|
17727
18234
|
init_errors();
|
|
17728
|
-
import { defineTool as
|
|
18235
|
+
import { defineTool as defineTool4 } from "@deepseek-ai/dsh-tools";
|
|
17729
18236
|
var DEFAULT_JOB_WAIT_MS = 3e4;
|
|
17730
18237
|
var MAX_JOB_WAIT_MS = 6e5;
|
|
17731
18238
|
var DEFAULT_JOB_POLL_MS = 500;
|
|
@@ -18036,7 +18543,7 @@ function clampInt2(value, fallback, min, max) {
|
|
|
18036
18543
|
}
|
|
18037
18544
|
function remoteJobToolDefinitions(backend) {
|
|
18038
18545
|
return [
|
|
18039
|
-
|
|
18546
|
+
defineTool4({
|
|
18040
18547
|
name: "remote_exec_background",
|
|
18041
18548
|
description: 'Start one shell command on the remote host as a background job and return immediately with its job id, instead of blocking until it finishes. Use it for anything that takes minutes: a full `m -j16` tree build, a test sweep, a long transfer. Read its output, wait for it, list jobs and cancel with `remote_job` (`action: "output" | "status" | "list" | "kill"`) \u2014 that tool also explains how output is retained. Pass `interactive: true` to keep stdin open so `remote_job { action: "stdin" }` can feed it later; without it the job reads EOF immediately.' + // 只保留「作业随连接结束」这一条:它必须出现在**启动**的地方,因为「后台 = 持久」正是这里最
|
|
18042
18549
|
// 容易产生的错误预期。轮询与环形缓冲的细节归 `remote_job`,那里才是读输出的地方。
|
|
@@ -18114,7 +18621,7 @@ function remoteJobToolDefinitions(backend) {
|
|
|
18114
18621
|
};
|
|
18115
18622
|
}
|
|
18116
18623
|
}),
|
|
18117
|
-
|
|
18624
|
+
defineTool4({
|
|
18118
18625
|
name: "remote_job",
|
|
18119
18626
|
description: "Observe or control a background job started with `remote_exec_background`; `action` decides which: `status` reads one job's state (still running, exit code, how long it ran, bytes produced, bytes dropped); `output` reads its output from a byte offset; `list` shows every job this connection has started, running and finished; `kill` requests termination \u2014 asynchronous, so this reports the request and `status` confirms the end, and the agent kills the whole process group; `stdin` writes to a job started with `interactive: true`, and `close: true` signals EOF; `forget` drops a settled job from the list so it stops crowding `list` (a running job is refused)." + JOB_LIFETIME_NOTE + JOB_POLL_NOTE + JOB_DROP_NOTE + " `list` is per connection: it is not a cross-host task queue.",
|
|
18120
18627
|
parameters: {
|
|
@@ -18354,7 +18861,7 @@ function registerRemoteJobTools(ctx, backend) {
|
|
|
18354
18861
|
|
|
18355
18862
|
// src/host/integration/fs/remote-file-tools.ts
|
|
18356
18863
|
init_errors();
|
|
18357
|
-
import { defineTool as
|
|
18864
|
+
import { defineTool as defineTool5 } from "@deepseek-ai/dsh-tools";
|
|
18358
18865
|
var DEFAULT_READ_LINES = 2e3;
|
|
18359
18866
|
var MAX_READ_LINES = 5e3;
|
|
18360
18867
|
var DEFAULT_MAX_LINE_LENGTH = 2e3;
|
|
@@ -18566,7 +19073,7 @@ function formatWrite(view) {
|
|
|
18566
19073
|
}
|
|
18567
19074
|
function requiredString3(value, field) {
|
|
18568
19075
|
if (typeof value !== "string" || value.trim() === "") throw new Error(`${field} \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32`);
|
|
18569
|
-
return value;
|
|
19076
|
+
return value.trim();
|
|
18570
19077
|
}
|
|
18571
19078
|
function clampInt3(value, fallback, min, max) {
|
|
18572
19079
|
const number = typeof value === "number" && Number.isFinite(value) ? Math.floor(value) : fallback;
|
|
@@ -18575,7 +19082,7 @@ function clampInt3(value, fallback, min, max) {
|
|
|
18575
19082
|
}
|
|
18576
19083
|
function remoteFileToolDefinitions(backend, allowMutation) {
|
|
18577
19084
|
return [
|
|
18578
|
-
|
|
19085
|
+
defineTool5({
|
|
18579
19086
|
name: "remote_read",
|
|
18580
19087
|
description: "Read one text file in the remote workspace this session is working in, with **1-based line numbers**, and return a window of it controlled by `offset` (first line) and `limit` (how many lines). Use this instead of `cat` through `remote_exec`: `remote_exec` truncates at 64 KiB and prints no line numbers. The result carries the file's `version` token \u2014 pass it back as `expectedVersion` to `remote_write` or `remote_edit` so a concurrent change is refused instead of overwritten. Read-only: calls to this tool may overlap with other read-only remote calls.",
|
|
18581
19088
|
// Read-only: it observes one remote file and changes nothing.
|
|
@@ -18657,7 +19164,7 @@ function remoteFileToolDefinitions(backend, allowMutation) {
|
|
|
18657
19164
|
};
|
|
18658
19165
|
}
|
|
18659
19166
|
}),
|
|
18660
|
-
|
|
19167
|
+
defineTool5({
|
|
18661
19168
|
name: "remote_write",
|
|
18662
19169
|
description: "Write one text file in the remote workspace this session is working in, replacing its whole content. Pass `expectedVersion` (from `remote_read`) to make the write refuse when the remote file changed since that read. An existing target without a version guard is **refused** unless `force: true`: a blind overwrite silently discards content this process never saw, including an edit made by another user on a shared machine. Parent directories are created when missing. Pass `mode` to set permission bits in the same call, which is what makes a written build or probe script runnable. NOT concurrency-safe: it changes remote state, so it never joins a parallel group.",
|
|
18663
19170
|
parameters: {
|
|
@@ -18735,7 +19242,7 @@ function remoteFileToolDefinitions(backend, allowMutation) {
|
|
|
18735
19242
|
};
|
|
18736
19243
|
}
|
|
18737
19244
|
}),
|
|
18738
|
-
|
|
19245
|
+
defineTool5({
|
|
18739
19246
|
name: "remote_edit",
|
|
18740
19247
|
description: "Replace a **literal** string inside one file in the remote workspace this session is working in. `old` is matched verbatim \u2014 a backslash-n in `old` matches the two characters `\\` and `n`, never a newline, so write real newlines when you mean them. `new` is inserted verbatim, so `$&`, `$1` and shell variables in it are not expanded. By default `old` must occur **exactly once**: zero matches and multiple matches are both refused, and the multiple-match refusal reports how many there were. Pass `all: true` to replace every occurrence on purpose. Prefer this over patching through `remote_exec` + python/sed: it cannot mis-escape your text, and `expectedVersion` (from `remote_read`) refuses the edit when the file changed in the meantime. NOT concurrency-safe: it changes remote state, so it never joins a parallel group.",
|
|
18741
19248
|
parameters: {
|
|
@@ -18817,7 +19324,7 @@ function remoteFileToolDefinitions(backend, allowMutation) {
|
|
|
18817
19324
|
};
|
|
18818
19325
|
}
|
|
18819
19326
|
}),
|
|
18820
|
-
|
|
19327
|
+
defineTool5({
|
|
18821
19328
|
name: "remote_patch",
|
|
18822
19329
|
description: "Apply a **unified diff** to one file in the remote workspace, atomically: every hunk lands in one round trip, and if any hunk fails, nothing is written. Use this instead of calling `remote_edit` three times \u2014 three edits are three read-modify-write cycles, and a failure in the middle leaves the file half changed. Hunks are located by **context, not by line number**: the numbers in `@@ -a,b +c,d @@` are used only in error messages, so a diff generated against a slightly older file still applies as long as the context is right. Each hunk's old text must occur exactly once \u2014 zero occurrences means the file changed, several means the context is not distinctive enough; both are refused rather than guessed at.",
|
|
18823
19330
|
parameters: {
|
|
@@ -18862,7 +19369,7 @@ function remoteFileToolDefinitions(backend, allowMutation) {
|
|
|
18862
19369
|
return { path, hunks: outcome.hunks, bytes: outcome.bytes, version: outcome.version };
|
|
18863
19370
|
}
|
|
18864
19371
|
}),
|
|
18865
|
-
|
|
19372
|
+
defineTool5({
|
|
18866
19373
|
name: "remote_move",
|
|
18867
19374
|
description: 'Move or rename one path in the remote workspace. `createParents` decides whether a missing destination directory is created (default true) or refused. A cross-device move **fails** rather than silently copying: a silent copy would turn "move then delete the source" into data loss when the delete lands first. Moving onto an existing directory is refused too \u2014 the caller meant a rename, and replacing a directory is not that. Use `remote_copy` when you want a copy that stays put. NOT concurrency-safe: it changes remote state, so it never joins a parallel group.',
|
|
18868
19375
|
parameters: {
|
|
@@ -18899,7 +19406,7 @@ function remoteFileToolDefinitions(backend, allowMutation) {
|
|
|
18899
19406
|
return { from, to, createParents };
|
|
18900
19407
|
}
|
|
18901
19408
|
}),
|
|
18902
|
-
|
|
19409
|
+
defineTool5({
|
|
18903
19410
|
name: "remote_stat",
|
|
18904
19411
|
description: 'Read one path\'s **metadata** in the remote workspace without reading its content: whether it exists, whether it is a file, directory or symlink, its size, its modification time and its version token. Use this instead of `remote_read` when you only need to know "does it exist / how big / when did it change" \u2014 reading a file to answer that costs the whole file. The `version` it returns is the same token `remote_read` reports, so it can be passed to `remote_write` / `remote_edit` / `remote_patch` as `expectedVersion`. NOT concurrency-safe: read-only, but it never joins a parallel group.',
|
|
18905
19412
|
parameters: {
|
|
@@ -19726,7 +20233,7 @@ ${describeInstalled(available)}`);
|
|
|
19726
20233
|
|
|
19727
20234
|
// src/host/integration/tools/forward-tools.ts
|
|
19728
20235
|
init_errors();
|
|
19729
|
-
import { defineTool as
|
|
20236
|
+
import { defineTool as defineTool6 } from "@deepseek-ai/dsh-tools";
|
|
19730
20237
|
function createForwardToolBackend(acquire) {
|
|
19731
20238
|
const records = /* @__PURE__ */ new Map();
|
|
19732
20239
|
let sequence2 = 0;
|
|
@@ -19788,7 +20295,7 @@ function text5(value) {
|
|
|
19788
20295
|
return [{ type: "text", text: value }];
|
|
19789
20296
|
}
|
|
19790
20297
|
function remoteForwardDefinition(backend) {
|
|
19791
|
-
return
|
|
20298
|
+
return defineTool6({
|
|
19792
20299
|
name: "remote_forward",
|
|
19793
20300
|
description: "Forward one remote TCP port to a local one, so a service the agent started on the remote host becomes reachable from this machine (and from the DSH browser sidebar). Typical use: start a dev server with `remote_exec_background`, then `remote_forward` its port and open the returned `http://127.0.0.1:<port>` URL. The local listener binds **127.0.0.1 only** \u2014 never the LAN; open it up via the host sshd's `GatewayPorts` if that is really what you want. `start` returns the real local port (auto-picked when omitted) plus a `forwardId`; `list` shows what is live; `stop` closes one and is idempotent. Forwards live as long as the connection and are closed automatically when the plugin unloads, so a forgotten one does not leak a port forever \u2014 but stop the ones you are done with.",
|
|
19794
20301
|
parameters: {
|
|
@@ -19891,7 +20398,7 @@ function registerRemoteForwardTool(ctx, backend) {
|
|
|
19891
20398
|
|
|
19892
20399
|
// src/host/integration/tools/remote-git-tools.ts
|
|
19893
20400
|
init_shell();
|
|
19894
|
-
import { defineTool as
|
|
20401
|
+
import { defineTool as defineTool7 } from "@deepseek-ai/dsh-tools";
|
|
19895
20402
|
var READ_ACTIONS = ["status", "diff", "log", "show", "branch"];
|
|
19896
20403
|
var WRITE_ACTIONS = ["add", "commit", "checkout", "restore", "stash"];
|
|
19897
20404
|
var ACTIONS = [...READ_ACTIONS, ...WRITE_ACTIONS];
|
|
@@ -20078,7 +20585,7 @@ function formatOutcome(value) {
|
|
|
20078
20585
|
${body}${suffix}`;
|
|
20079
20586
|
}
|
|
20080
20587
|
function remoteGitDefinition(backend) {
|
|
20081
|
-
return
|
|
20588
|
+
return defineTool7({
|
|
20082
20589
|
name: "remote_git",
|
|
20083
20590
|
description: "Run a git operation on the remote repository this session is working in, and return its **parsed** result instead of raw terminal text. Prefer this over `remote_exec git ... : the parse is done here (branch/ahead/behind, status entries, log entries), so a new git version's wording cannot break your reading. Read-only actions: `status`, `diff`, `log`, `show`, `branch`. Mutating actions: `add`, `commit`, `checkout`, `restore`, `stash` \u2014 these change the user's repository, so state the intent in your message first. Deliberately **not** offered: `reset --hard`, `clean -fd`, `push --force`, `checkout .`. Those have no safe preview; use `remote_exec` explicitly if you really need one. Git is run with `GIT_TERMINAL_PROMPT=0`, so a missing credential **fails** instead of hanging on a password prompt. Read-only actions may overlap with other read-only remote calls.",
|
|
20084
20591
|
parameters: {
|
|
@@ -20252,7 +20759,7 @@ function registerRemoteGitTool(ctx, backend) {
|
|
|
20252
20759
|
}
|
|
20253
20760
|
|
|
20254
20761
|
// src/host/integration/tools/remote-lsp-tools.ts
|
|
20255
|
-
import { defineTool as
|
|
20762
|
+
import { defineTool as defineTool8 } from "@deepseek-ai/dsh-tools";
|
|
20256
20763
|
function toView(diagnostic) {
|
|
20257
20764
|
return {
|
|
20258
20765
|
line: diagnostic.line + 1,
|
|
@@ -20304,7 +20811,7 @@ function parseServer(value) {
|
|
|
20304
20811
|
function remoteLspToolDefinitions(deps) {
|
|
20305
20812
|
const { backend, registry } = deps;
|
|
20306
20813
|
return [
|
|
20307
|
-
|
|
20814
|
+
defineTool8({
|
|
20308
20815
|
name: "remote_lsp",
|
|
20309
20816
|
description: 'Ask a **language server running on the remote host** about a file: diagnostics, hover, or definition. Use this instead of running the compiler and reading its text output \u2014 the answer is structured and scoped to one document, and it is the only way to ask "what does this symbol mean" or "where is this defined". The server is started on first use and reused; it takes seconds to index a project, so the first call is slow and later ones are fast. Line and column are **1-based**, matching `remote_read`. Requires a language server installed on the remote host (gopls, rust-analyzer, typescript-language-server, pylsp, clangd); pass `server` to use a different one. NOT concurrency-safe: it changes remote state (starts a process), so it never joins a parallel group.',
|
|
20310
20817
|
parameters: {
|
|
@@ -20517,7 +21024,7 @@ import { homedir as homedir6 } from "node:os";
|
|
|
20517
21024
|
|
|
20518
21025
|
// src/host/integration/session/session-labels.ts
|
|
20519
21026
|
function normalizePath(path) {
|
|
20520
|
-
return path
|
|
21027
|
+
return foldPathKey(path);
|
|
20521
21028
|
}
|
|
20522
21029
|
function createSessionTitleResolver(ctx) {
|
|
20523
21030
|
return (header) => {
|
|
@@ -20587,18 +21094,28 @@ function wireSessionAdmin(env) {
|
|
|
20587
21094
|
listDir: async (path) => {
|
|
20588
21095
|
try {
|
|
20589
21096
|
const entries = await readdir6(path, { withFileTypes: true });
|
|
20590
|
-
const
|
|
20591
|
-
|
|
20592
|
-
|
|
20593
|
-
|
|
20594
|
-
|
|
20595
|
-
const
|
|
20596
|
-
|
|
20597
|
-
if (
|
|
20598
|
-
|
|
21097
|
+
const statLimit = 16;
|
|
21098
|
+
const out = new Array(entries.length);
|
|
21099
|
+
let next = 0;
|
|
21100
|
+
const workers = Array.from({ length: Math.max(1, Math.min(statLimit, entries.length)) }, async () => {
|
|
21101
|
+
for (; ; ) {
|
|
21102
|
+
const index = next;
|
|
21103
|
+
next += 1;
|
|
21104
|
+
if (index >= entries.length) return;
|
|
21105
|
+
const entry = entries[index];
|
|
21106
|
+
if (entry === void 0) return;
|
|
21107
|
+
let bytes = 0;
|
|
21108
|
+
let mtimeMs = 0;
|
|
21109
|
+
try {
|
|
21110
|
+
const info = await nodeStat2(join17(path, entry.name));
|
|
21111
|
+
mtimeMs = info.mtimeMs;
|
|
21112
|
+
if (entry.isFile()) bytes = info.size;
|
|
21113
|
+
} catch {
|
|
21114
|
+
}
|
|
21115
|
+
out[index] = { name: entry.name, isDirectory: entry.isDirectory(), bytes, mtimeMs };
|
|
20599
21116
|
}
|
|
20600
|
-
|
|
20601
|
-
|
|
21117
|
+
});
|
|
21118
|
+
await Promise.all(workers);
|
|
20602
21119
|
return out;
|
|
20603
21120
|
} catch (error) {
|
|
20604
21121
|
if (error.code === "ENOENT") return [];
|
|
@@ -20690,7 +21207,7 @@ init_errors();
|
|
|
20690
21207
|
|
|
20691
21208
|
// src/host/integration/tools/management-tools.ts
|
|
20692
21209
|
import { isAbsolute as isAbsolute3, resolve as resolve2 } from "node:path";
|
|
20693
|
-
import { defineTool as
|
|
21210
|
+
import { defineTool as defineTool9 } from "@deepseek-ai/dsh-tools";
|
|
20694
21211
|
function text7(value) {
|
|
20695
21212
|
return [{ type: "text", text: value }];
|
|
20696
21213
|
}
|
|
@@ -20765,7 +21282,7 @@ function formatEnv(result) {
|
|
|
20765
21282
|
}
|
|
20766
21283
|
function managementToolDefinitions(backend) {
|
|
20767
21284
|
return [
|
|
20768
|
-
|
|
21285
|
+
defineTool9({
|
|
20769
21286
|
name: "remote_transfer",
|
|
20770
21287
|
description: "Upload one local file to the remote workspace this session is working in, or download one remote file to the local machine. Chunked with progress; pass `resume: true` to continue an interrupted transfer instead of rewriting the whole file (the prefix is **compared with the source** before it is kept, so a changed source degrades to a full retransfer instead of splicing two files). The result reports bytes, chunk count, whether it resumed, the absolute local path the bytes landed at, and \u2014 when the host can hash \u2014 the digest of the content that actually landed. `localPath` must be **absolute**: the plugin resolves it in its own process, so a relative spelling would land under the plugin's working directory instead of anywhere the caller expects. An upload that would overwrite an **existing** remote file is refused unless you pass `expectedVersion` (from `remote_stat`/`remote_read`) or `force: true`: a silent overwrite is the one operation that destroys someone else's work without a trace.",
|
|
20771
21288
|
parameters: {
|
|
@@ -20903,7 +21420,7 @@ function managementToolDefinitions(backend) {
|
|
|
20903
21420
|
};
|
|
20904
21421
|
}
|
|
20905
21422
|
}),
|
|
20906
|
-
|
|
21423
|
+
defineTool9({
|
|
20907
21424
|
name: "remote_delete",
|
|
20908
21425
|
description: 'Delete one path inside the remote workspace. `mode: "recycle"` (the default) moves it to the workspace recycle bin, where it is recoverable and can be undone; `mode: "permanent"` removes it for good and requires user approval. Omitting `mode` is therefore the safe call, and the result always names which mode ran.',
|
|
20909
21426
|
parameters: {
|
|
@@ -20951,7 +21468,7 @@ function managementToolDefinitions(backend) {
|
|
|
20951
21468
|
};
|
|
20952
21469
|
}
|
|
20953
21470
|
}),
|
|
20954
|
-
|
|
21471
|
+
defineTool9({
|
|
20955
21472
|
name: "remote_restore",
|
|
20956
21473
|
description: "Undo a delete. With `id`, puts that bin entry back at the path it was deleted from; without `id`, undoes the most recent discard; with `list: true`, shows what the bin holds for this session's root and changes nothing. `remote_delete` returns the entry id as `entryId`. The bin is subject to retention limits (`binMaxEntries` / `binMaxBytes` / `binMaxAgeMs`), so an old entry may already be gone.",
|
|
20957
21474
|
parameters: {
|
|
@@ -21013,7 +21530,7 @@ function managementToolDefinitions(backend) {
|
|
|
21013
21530
|
return { action: "undone", id: outcome.id, message: outcome.message };
|
|
21014
21531
|
}
|
|
21015
21532
|
}),
|
|
21016
|
-
|
|
21533
|
+
defineTool9({
|
|
21017
21534
|
name: "remote_copy",
|
|
21018
21535
|
description: "Copy one file to another path **on the remote host**. Both paths are inside this session's remote root. On a Tier 2 (agent) connection the copy happens on the remote side, so the bytes never cross this machine; otherwise they are relayed through it, and the result says which happened in `via`. Pass `recursive: true` to copy a whole directory tree in one round trip (Tier 2 only \u2014 it refuses rather than relaying a tree file by file). The destination must not exist, or be an empty directory: a recursive copy never deletes anything.",
|
|
21019
21536
|
parameters: {
|
|
@@ -21076,7 +21593,7 @@ function managementToolDefinitions(backend) {
|
|
|
21076
21593
|
};
|
|
21077
21594
|
}
|
|
21078
21595
|
}),
|
|
21079
|
-
|
|
21596
|
+
defineTool9({
|
|
21080
21597
|
name: "remote_env",
|
|
21081
21598
|
description: "Sample what the remote host is and what it can do: OS/arch/distro/kernel, shell, CPU count, memory and disk, user and home, and **which commands exist** (rg, fd, git, python3, gcc, docker, ...). Ask this once instead of guessing and retrying: the answer is cached on the agent until `refresh: true`, so a repeat call costs one round trip and no resampling.",
|
|
21082
21599
|
parameters: {
|