dsh-remote-vs 0.0.0-stage → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.en.md +385 -0
- package/README.md +347 -2
- package/cordis.patch.yml +259 -0
- package/icon.svg +7 -0
- package/lib/client.js +62602 -0
- package/lib/index.js +4287 -0
- package/lib/p2-bridge-refs.js +10 -0
- package/lib/p2-bridge.js +16 -0
- package/lib/p2-router-fs.js +252 -0
- package/lib/p2-router-refs.js +466 -0
- package/lib/p2-router-spill.js +62 -0
- package/lib/p2-router-subprocess.js +64 -0
- package/lib/p2-shell-bash.js +24 -0
- package/lib/ssh-proc-arI5lm4W.js +632 -0
- package/lib/types/changes/recorder.d.ts +240 -0
- package/lib/types/changes/status.d.ts +201 -0
- package/lib/types/changes/text-diff.d.ts +65 -0
- package/lib/types/client/CodeEditor.d.ts +125 -0
- package/lib/types/client/Dialog.d.ts +14 -0
- package/lib/types/client/DirectoryFlow.d.ts +30 -0
- package/lib/types/client/EditorPane.d.ts +30 -0
- package/lib/types/client/HostsView.d.ts +10 -0
- package/lib/types/client/NativeExplorer.d.ts +39 -0
- package/lib/types/client/RemoteDirectoryPicker.d.ts +16 -0
- package/lib/types/client/RemoteEditorView.d.ts +20 -0
- package/lib/types/client/RemoteExplorer.d.ts +18 -0
- package/lib/types/client/RemoteRoot.d.ts +16 -0
- package/lib/types/client/SearchView.d.ts +14 -0
- package/lib/types/client/SettingsHosts.d.ts +15 -0
- package/lib/types/client/TerminalView.d.ts +16 -0
- package/lib/types/client/WorkspaceWizard.d.ts +16 -0
- package/lib/types/client/WorkspacesView.d.ts +14 -0
- package/lib/types/client/XTerm.d.ts +15 -0
- package/lib/types/client/api.d.ts +162 -0
- package/lib/types/client/changes/ChangesDock.d.ts +35 -0
- package/lib/types/client/changes/model.d.ts +36 -0
- package/lib/types/client/changes/undo-affordance.d.ts +48 -0
- package/lib/types/client/changes/undo-context.d.ts +25 -0
- package/lib/types/client/composer.d.ts +165 -0
- package/lib/types/client/i18n.d.ts +17 -0
- package/lib/types/client/index.d.ts +14 -0
- package/lib/types/client/remote-file-open-target.d.ts +247 -0
- package/lib/types/client/remote-file-open.d.ts +35 -0
- package/lib/types/client/reveal.d.ts +15 -0
- package/lib/types/client/scm/DiffView.d.ts +24 -0
- package/lib/types/client/scm/HistoryPanel.d.ts +12 -0
- package/lib/types/client/scm/RepoPicker.d.ts +24 -0
- package/lib/types/client/scm/ScmPanel.d.ts +15 -0
- package/lib/types/client/scm/actions.d.ts +170 -0
- package/lib/types/client/scm/diff-model.d.ts +29 -0
- package/lib/types/client/scm/stage-queue.d.ts +99 -0
- package/lib/types/client/scm/status-map.d.ts +157 -0
- package/lib/types/client/scm/untracked-dirs.d.ts +72 -0
- package/lib/types/client/selection-popup.d.ts +61 -0
- package/lib/types/client/store.d.ts +437 -0
- package/lib/types/client/theme.d.ts +15 -0
- package/lib/types/client/ui.d.ts +46 -0
- package/lib/types/client/upload-plan.d.ts +78 -0
- package/lib/types/client/upload-source.d.ts +46 -0
- package/lib/types/client/use-remote-chat-target.d.ts +17 -0
- package/lib/types/client/vsc/FindWidget.d.ts +81 -0
- package/lib/types/client/vsc/change-marks.d.ts +150 -0
- package/lib/types/client/vsc/editor-langs.d.ts +5 -0
- package/lib/types/client/vsc/editor-palette.d.ts +37 -0
- package/lib/types/client/vsc/editor-theme.d.ts +32 -0
- package/lib/types/client/vsc/fallback-highlight.d.ts +26 -0
- package/lib/types/client/vsc/kit.d.ts +195 -0
- package/lib/types/client/vsc/markdown.d.ts +11 -0
- package/lib/types/client/vsc/reveal-line.d.ts +38 -0
- package/lib/types/client/vsc/tokenize.d.ts +104 -0
- package/lib/types/deploy.d.ts +94 -0
- package/lib/types/fs-ops.d.ts +8 -0
- package/lib/types/git-format.d.ts +34 -0
- package/lib/types/git-ops.d.ts +183 -0
- package/lib/types/http.d.ts +14 -0
- package/lib/types/importers.d.ts +20 -0
- package/lib/types/index.d.ts +87 -0
- package/lib/types/keygen.d.ts +19 -0
- package/lib/types/loopback.d.ts +7 -0
- package/lib/types/p2/bridge-refs.d.ts +21 -0
- package/lib/types/p2/bridge.d.ts +24 -0
- package/lib/types/p2/file-refs.d.ts +73 -0
- package/lib/types/p2/helper-rpc.d.ts +63 -0
- package/lib/types/p2/in-flight-gate.d.ts +25 -0
- package/lib/types/p2/router-fs.d.ts +92 -0
- package/lib/types/p2/router-refs.d.ts +45 -0
- package/lib/types/p2/router-spill.d.ts +11 -0
- package/lib/types/p2/router-subprocess.d.ts +23 -0
- package/lib/types/p2/shell-bash.d.ts +16 -0
- package/lib/types/p2/ssh-proc.d.ts +22 -0
- package/lib/types/p2/worlds.d.ts +179 -0
- package/lib/types/paths.d.ts +12 -0
- package/lib/types/pool.d.ts +105 -0
- package/lib/types/protocol.d.ts +452 -0
- package/lib/types/routes.d.ts +27 -0
- package/lib/types/search-ops.d.ts +68 -0
- package/lib/types/sftp.d.ts +90 -0
- package/lib/types/store.d.ts +66 -0
- package/lib/types/terminal.d.ts +51 -0
- package/lib/types/timeouts.d.ts +83 -0
- package/lib/types/tools.d.ts +41 -0
- package/lib/worlds-PUumRIh7.js +18765 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +189 -4
- package/src/changes/recorder.ts +902 -0
- package/src/changes/status.ts +448 -0
- package/src/changes/text-diff.ts +377 -0
- package/src/client/CodeEditor.tsx +923 -0
- package/src/client/Dialog.tsx +76 -0
- package/src/client/DirectoryFlow.tsx +431 -0
- package/src/client/EditorPane.tsx +512 -0
- package/src/client/HostsView.tsx +312 -0
- package/src/client/NativeExplorer.tsx +121 -0
- package/src/client/RemoteDirectoryPicker.tsx +157 -0
- package/src/client/RemoteEditorView.tsx +54 -0
- package/src/client/RemoteExplorer.tsx +1398 -0
- package/src/client/RemoteRoot.tsx +389 -0
- package/src/client/SearchView.tsx +407 -0
- package/src/client/SettingsHosts.tsx +136 -0
- package/src/client/TerminalView.tsx +308 -0
- package/src/client/WorkspaceWizard.tsx +199 -0
- package/src/client/WorkspacesView.tsx +93 -0
- package/src/client/XTerm.tsx +308 -0
- package/src/client/api.ts +331 -0
- package/src/client/changes/ChangesDock.tsx +348 -0
- package/src/client/changes/changes.css +230 -0
- package/src/client/changes/model.ts +347 -0
- package/src/client/changes/undo-affordance.ts +389 -0
- package/src/client/changes/undo-context.ts +34 -0
- package/src/client/composer.ts +436 -0
- package/src/client/css.d.ts +7 -0
- package/src/client/i18n.ts +702 -0
- package/src/client/index.tsx +286 -0
- package/src/client/remote-file-open-target.ts +553 -0
- package/src/client/remote-file-open.ts +261 -0
- package/src/client/reveal.ts +33 -0
- package/src/client/scm/DiffView.tsx +570 -0
- package/src/client/scm/HistoryPanel.tsx +353 -0
- package/src/client/scm/RepoPicker.tsx +238 -0
- package/src/client/scm/ScmPanel.tsx +857 -0
- package/src/client/scm/actions.ts +460 -0
- package/src/client/scm/diff-model.ts +206 -0
- package/src/client/scm/stage-queue.ts +264 -0
- package/src/client/scm/status-map.ts +381 -0
- package/src/client/scm/untracked-dirs.ts +139 -0
- package/src/client/selection-popup.ts +157 -0
- package/src/client/store.tsx +1670 -0
- package/src/client/theme.ts +54 -0
- package/src/client/ui.tsx +136 -0
- package/src/client/upload-plan.ts +153 -0
- package/src/client/upload-source.ts +145 -0
- package/src/client/use-remote-chat-target.ts +153 -0
- package/src/client/vsc/FindWidget.tsx +446 -0
- package/src/client/vsc/change-marks.ts +586 -0
- package/src/client/vsc/editor-langs.ts +137 -0
- package/src/client/vsc/editor-palette.ts +107 -0
- package/src/client/vsc/editor-theme.ts +93 -0
- package/src/client/vsc/fallback-highlight.ts +145 -0
- package/src/client/vsc/kit.tsx +777 -0
- package/src/client/vsc/markdown.tsx +172 -0
- package/src/client/vsc/reveal-line.ts +142 -0
- package/src/client/vsc/tokenize.ts +569 -0
- package/src/client/vsc/vsc.css +2669 -0
- package/src/deploy.ts +481 -0
- package/src/fs-ops.ts +57 -0
- package/src/git-format.ts +205 -0
- package/src/git-ops.ts +693 -0
- package/src/http.ts +55 -0
- package/src/importers.ts +141 -0
- package/src/index.ts +442 -0
- package/src/keygen.ts +77 -0
- package/src/loopback.ts +47 -0
- package/src/p2/bridge-refs.ts +27 -0
- package/src/p2/bridge.ts +32 -0
- package/src/p2/file-refs.ts +392 -0
- package/src/p2/helper-rpc.ts +394 -0
- package/src/p2/in-flight-gate.ts +63 -0
- package/src/p2/router-fs.ts +327 -0
- package/src/p2/router-refs.ts +166 -0
- package/src/p2/router-spill.ts +75 -0
- package/src/p2/router-subprocess.ts +92 -0
- package/src/p2/shell-bash.ts +30 -0
- package/src/p2/ssh-proc.ts +802 -0
- package/src/p2/worlds.ts +735 -0
- package/src/paths.ts +50 -0
- package/src/pool.ts +811 -0
- package/src/protocol.ts +460 -0
- package/src/routes.ts +802 -0
- package/src/search-ops.ts +220 -0
- package/src/sftp.ts +677 -0
- package/src/store.ts +364 -0
- package/src/terminal.ts +291 -0
- package/src/timeouts.ts +122 -0
- package/src/tools.ts +320 -0
- package/vendor/dsh-remote-helpers/dsh-ssh-helper-0.2.1-alpha.2-linux-x64.helper.json +16 -0
- package/vendor/dsh-remote-helpers/dsh-ssh-helper-0.2.1-alpha.2-linux-x64.tar.gz +4 -0
- package/vendor/dsh-remote-helpers/dsh-ssh-helper-0.2.1-alpha.2-linux-x64.tar.gz.sha256 +1 -0
- package/vendor/dsh-remote-helpers/glibc-ceiling.patch +31 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
import { FileReferenceService } from "@deepseek-ai/dsh-file-reference";
|
|
2
|
+
/** Mirrored stock exclusion list (order kept for diffability). */
|
|
3
|
+
const REMOTE_FILE_SEARCH_EXCLUDED_DIRECTORIES = [
|
|
4
|
+
".git",
|
|
5
|
+
"node_modules",
|
|
6
|
+
"dist",
|
|
7
|
+
"build",
|
|
8
|
+
"out",
|
|
9
|
+
"coverage",
|
|
10
|
+
"target",
|
|
11
|
+
".next",
|
|
12
|
+
".nuxt",
|
|
13
|
+
".turbo",
|
|
14
|
+
".venv",
|
|
15
|
+
"__pycache__",
|
|
16
|
+
".pytest_cache",
|
|
17
|
+
".mypy_cache",
|
|
18
|
+
".gradle"
|
|
19
|
+
];
|
|
20
|
+
/** Byte ceiling for one rg listing transfer (50k lines worst case). */
|
|
21
|
+
const RG_OUTPUT_MAX_BYTES = 16 * 1024 * 1024;
|
|
22
|
+
var RemoteFileSearch = class {
|
|
23
|
+
deps;
|
|
24
|
+
excluded;
|
|
25
|
+
settled;
|
|
26
|
+
pending;
|
|
27
|
+
pendingAbort;
|
|
28
|
+
/** Invalidation generation the pending scan belongs to (-1: none). */
|
|
29
|
+
pendingGeneration = -1;
|
|
30
|
+
disposed = false;
|
|
31
|
+
/** Monotonic invalidation counter; a scan started below it is stale. */
|
|
32
|
+
invalidations = 0;
|
|
33
|
+
constructor(deps) {
|
|
34
|
+
this.deps = deps;
|
|
35
|
+
this.excluded = new Set(REMOTE_FILE_SEARCH_EXCLUDED_DIRECTORIES);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Drop the cached index (tool activity); the next bare query rebuilds.
|
|
39
|
+
*
|
|
40
|
+
* Dropping — rather than keeping the entries and revalidating behind the
|
|
41
|
+
* caret the way the stock local service does — is deliberate: nothing local
|
|
42
|
+
* can tell whether a remote tree moved, so an index that could predate a
|
|
43
|
+
* change must cost a rebuild instead of answering. For the same reason a
|
|
44
|
+
* minimum rebuild interval is not used to coalesce a burst; the burst is
|
|
45
|
+
* damped where it is generated, by invalidating for write-capable tools in
|
|
46
|
+
* router-refs.ts.
|
|
47
|
+
*
|
|
48
|
+
* Aborting an in-flight scan is part of that: its result can no longer be
|
|
49
|
+
* published, and a query arriving after this call must not join it.
|
|
50
|
+
*/
|
|
51
|
+
invalidate() {
|
|
52
|
+
this.invalidations += 1;
|
|
53
|
+
this.settled = void 0;
|
|
54
|
+
if (this.pendingAbort !== void 0) {
|
|
55
|
+
try {
|
|
56
|
+
this.pendingAbort.abort(/* @__PURE__ */ new Error("file search index invalidated"));
|
|
57
|
+
} catch {}
|
|
58
|
+
this.pending = void 0;
|
|
59
|
+
this.pendingAbort = void 0;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
dispose() {
|
|
63
|
+
this.disposed = true;
|
|
64
|
+
this.invalidate();
|
|
65
|
+
}
|
|
66
|
+
/** Ranked candidates for one @ token (mirrors WorkspaceFileSearch.list). */
|
|
67
|
+
async list(rawQuery, signal) {
|
|
68
|
+
signal.throwIfAborted();
|
|
69
|
+
if (this.disposed) return [];
|
|
70
|
+
const query = rawQuery.replaceAll("\\", "/");
|
|
71
|
+
const slash = query.lastIndexOf("/");
|
|
72
|
+
if (query === "" || slash >= 0) {
|
|
73
|
+
const directory = slash < 0 ? "" : query.slice(0, slash + 1);
|
|
74
|
+
const fragment = slash < 0 ? "" : query.slice(slash + 1);
|
|
75
|
+
return this.listDirectory(directory, fragment, signal);
|
|
76
|
+
}
|
|
77
|
+
return rankCandidates((await this.indexFor(signal)).filter((c) => visibleForGlobalQuery(c.path, query)), query, 20);
|
|
78
|
+
}
|
|
79
|
+
/** Live single-level listing (one RPC; mirrors listDirectory exactly). */
|
|
80
|
+
async listDirectory(displayDirectory, fragment, signal) {
|
|
81
|
+
if (displayDirectory.split("/").some((segment) => this.excluded.has(segment))) return [];
|
|
82
|
+
const absolute = joinRemote(this.deps.remoteRoot, displayDirectory);
|
|
83
|
+
if (absolute === void 0) return [];
|
|
84
|
+
let entries;
|
|
85
|
+
try {
|
|
86
|
+
const target = await this.deps.fs.resolve(absolute, {});
|
|
87
|
+
entries = await this.deps.fs.listDir(target, signal);
|
|
88
|
+
} catch {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
const candidates = [];
|
|
92
|
+
for (const entry of entries) {
|
|
93
|
+
if (entry.name.startsWith(".") && !fragment.startsWith(".")) continue;
|
|
94
|
+
if (entry.type === "directory") {
|
|
95
|
+
if (this.excluded.has(entry.name)) continue;
|
|
96
|
+
candidates.push({
|
|
97
|
+
path: `${displayDirectory}${entry.name}`,
|
|
98
|
+
kind: "directory"
|
|
99
|
+
});
|
|
100
|
+
} else if (entry.type === "file") candidates.push({
|
|
101
|
+
path: `${displayDirectory}${entry.name}`,
|
|
102
|
+
kind: "file"
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return rankCandidates(candidates, fragment, 20);
|
|
106
|
+
}
|
|
107
|
+
/** Cached full listing; concurrent callers share one in-flight build. */
|
|
108
|
+
async indexFor(signal) {
|
|
109
|
+
if (this.settled !== void 0) return this.settled;
|
|
110
|
+
if (this.pending === void 0 || this.pendingGeneration !== this.invalidations) {
|
|
111
|
+
const controller = new AbortController();
|
|
112
|
+
const startedAt = this.invalidations;
|
|
113
|
+
this.pendingGeneration = startedAt;
|
|
114
|
+
this.pendingAbort = controller;
|
|
115
|
+
this.pending = this.buildIndex(controller.signal).then((entries) => {
|
|
116
|
+
if (this.pendingAbort === controller) {
|
|
117
|
+
this.pending = void 0;
|
|
118
|
+
this.pendingAbort = void 0;
|
|
119
|
+
}
|
|
120
|
+
if (!this.disposed && this.invalidations === startedAt) this.settled = entries;
|
|
121
|
+
return entries;
|
|
122
|
+
}, (error) => {
|
|
123
|
+
if (this.pendingAbort === controller) {
|
|
124
|
+
this.pending = void 0;
|
|
125
|
+
this.pendingAbort = void 0;
|
|
126
|
+
}
|
|
127
|
+
throw error;
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
signal.throwIfAborted();
|
|
131
|
+
return new Promise((resolve, reject) => {
|
|
132
|
+
const onAbort = () => {
|
|
133
|
+
signal.removeEventListener("abort", onAbort);
|
|
134
|
+
reject(signal.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("file search aborted"));
|
|
135
|
+
};
|
|
136
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
137
|
+
this.pending.then((entries) => {
|
|
138
|
+
signal.removeEventListener("abort", onAbort);
|
|
139
|
+
resolve(entries);
|
|
140
|
+
}, (error) => {
|
|
141
|
+
signal.removeEventListener("abort", onAbort);
|
|
142
|
+
reject(error);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
/** One remote `rg --files` run, head-capped, sorted for determinism. */
|
|
147
|
+
async buildIndex(signal) {
|
|
148
|
+
const globs = [];
|
|
149
|
+
for (const name of this.excluded) globs.push(`--glob=!**/${name}`, `--glob=!**/${name}/**`);
|
|
150
|
+
const argv = [
|
|
151
|
+
"rg",
|
|
152
|
+
"--no-config",
|
|
153
|
+
"--files",
|
|
154
|
+
"--no-ignore",
|
|
155
|
+
"--hidden",
|
|
156
|
+
...globs,
|
|
157
|
+
"--",
|
|
158
|
+
"."
|
|
159
|
+
];
|
|
160
|
+
let handle;
|
|
161
|
+
try {
|
|
162
|
+
handle = this.deps.spawn({
|
|
163
|
+
argv,
|
|
164
|
+
cwd: this.deps.remoteRoot,
|
|
165
|
+
stdio: {
|
|
166
|
+
stdin: "ignore",
|
|
167
|
+
stdout: "pipe",
|
|
168
|
+
stderr: { maxBytes: 4096 }
|
|
169
|
+
},
|
|
170
|
+
graceMs: 3e3,
|
|
171
|
+
signal
|
|
172
|
+
});
|
|
173
|
+
} catch (error) {
|
|
174
|
+
this.warn(`remote file index spawn failed: ${messageOf(error)}`);
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
const lines = [];
|
|
178
|
+
let bytes = 0;
|
|
179
|
+
let truncated = false;
|
|
180
|
+
try {
|
|
181
|
+
const stdout = handle.stdout;
|
|
182
|
+
if (stdout === void 0) handle.terminate();
|
|
183
|
+
else {
|
|
184
|
+
let carry = "";
|
|
185
|
+
const onAbort = () => {
|
|
186
|
+
try {
|
|
187
|
+
handle.terminate();
|
|
188
|
+
} catch {}
|
|
189
|
+
};
|
|
190
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
191
|
+
try {
|
|
192
|
+
for await (const chunk of stdout) {
|
|
193
|
+
const parts = (carry + (typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8"))).split("\n");
|
|
194
|
+
carry = parts.pop() ?? "";
|
|
195
|
+
for (const line of parts) {
|
|
196
|
+
const clean = line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
197
|
+
if (clean === "") continue;
|
|
198
|
+
bytes += Buffer.byteLength(clean, "utf8") + 1;
|
|
199
|
+
if (lines.length >= 5e4 || bytes > RG_OUTPUT_MAX_BYTES) {
|
|
200
|
+
truncated = true;
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
lines.push(clean);
|
|
204
|
+
}
|
|
205
|
+
if (truncated) break;
|
|
206
|
+
}
|
|
207
|
+
if (!truncated && carry !== "") {
|
|
208
|
+
if (lines.length < 5e4 && bytes <= RG_OUTPUT_MAX_BYTES) lines.push(carry.endsWith("\r") ? carry.slice(0, -1) : carry);
|
|
209
|
+
}
|
|
210
|
+
} finally {
|
|
211
|
+
signal.removeEventListener("abort", onAbort);
|
|
212
|
+
}
|
|
213
|
+
signal.throwIfAborted();
|
|
214
|
+
handle.terminate();
|
|
215
|
+
}
|
|
216
|
+
const outcome = await handle.done.catch(() => void 0);
|
|
217
|
+
if (outcome !== void 0 && outcome.exitCode !== 0 && outcome.exitCode !== 1) {
|
|
218
|
+
this.warn(`remote file index rg exited ${outcome.exitCode ?? outcome.signal}`);
|
|
219
|
+
return [];
|
|
220
|
+
}
|
|
221
|
+
} catch (error) {
|
|
222
|
+
try {
|
|
223
|
+
handle.terminate();
|
|
224
|
+
} catch {}
|
|
225
|
+
if (signal.aborted) throw error;
|
|
226
|
+
this.warn(`remote file index failed: ${messageOf(error)}`);
|
|
227
|
+
return [];
|
|
228
|
+
}
|
|
229
|
+
const files = /* @__PURE__ */ new Set();
|
|
230
|
+
for (let line of lines) {
|
|
231
|
+
if (line.startsWith("./")) line = line.slice(2);
|
|
232
|
+
if (line === "" || line === "." || line.startsWith("/")) continue;
|
|
233
|
+
if (line.split("/").some((segment) => segment === "" || segment === "." || segment === ".." || this.excluded.has(segment))) continue;
|
|
234
|
+
files.add(line);
|
|
235
|
+
}
|
|
236
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
237
|
+
for (const file of files) {
|
|
238
|
+
const segments = file.split("/");
|
|
239
|
+
for (let depth = 1; depth < segments.length; depth += 1) {
|
|
240
|
+
const prefix = segments.slice(0, depth).join("/");
|
|
241
|
+
const base = segments[depth - 1];
|
|
242
|
+
if (!this.excluded.has(base)) dirs.add(prefix);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const indexed = [...[...dirs].map((path) => ({
|
|
246
|
+
path,
|
|
247
|
+
kind: "directory"
|
|
248
|
+
})), ...[...files].map((path) => ({
|
|
249
|
+
path,
|
|
250
|
+
kind: "file"
|
|
251
|
+
}))];
|
|
252
|
+
indexed.sort((a, b) => compareText(a.path, b.path));
|
|
253
|
+
return indexed;
|
|
254
|
+
}
|
|
255
|
+
warn(message) {
|
|
256
|
+
try {
|
|
257
|
+
this.deps.warn?.(`[remote-refs] ${message}`);
|
|
258
|
+
} catch {}
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
/** Join a display dir under the remote root; undefined on jail escape. */
|
|
262
|
+
function joinRemote(root, display) {
|
|
263
|
+
const out = [];
|
|
264
|
+
for (const segment of display.split("/")) {
|
|
265
|
+
if (segment === "" || segment === ".") continue;
|
|
266
|
+
if (segment === "..") {
|
|
267
|
+
if (out.length === 0) return void 0;
|
|
268
|
+
out.pop();
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (out.length === 0 && /^[A-Za-z]:/.test(segment)) return void 0;
|
|
272
|
+
out.push(segment);
|
|
273
|
+
}
|
|
274
|
+
const base = root.endsWith("/") && root !== "/" ? root.slice(0, -1) : root;
|
|
275
|
+
return out.length === 0 ? base : `${base}/${out.join("/")}`;
|
|
276
|
+
}
|
|
277
|
+
/** Ported scoring (dsh-file-reference-local rankCandidates family, exact). */
|
|
278
|
+
function rankCandidates(candidates, query, limit) {
|
|
279
|
+
const ranked = [];
|
|
280
|
+
for (const candidate of candidates) {
|
|
281
|
+
const score = scoreCandidate(candidate, query);
|
|
282
|
+
if (score !== void 0) ranked.push({
|
|
283
|
+
candidate,
|
|
284
|
+
score
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
ranked.sort((left, right) => right.score - left.score || kindRank(left.candidate.kind) - kindRank(right.candidate.kind) || (query === "" ? 0 : left.candidate.path.length - right.candidate.path.length) || compareText(left.candidate.path, right.candidate.path));
|
|
288
|
+
return ranked.slice(0, limit).map((entry) => entry.candidate);
|
|
289
|
+
}
|
|
290
|
+
function scoreCandidate(candidate, query) {
|
|
291
|
+
if (query === "") return 0;
|
|
292
|
+
const path = candidate.path.toLowerCase();
|
|
293
|
+
const name = path.slice(path.lastIndexOf("/") + 1);
|
|
294
|
+
const needle = query.toLowerCase();
|
|
295
|
+
const directoryBonus = candidate.kind === "directory" ? 25 : 0;
|
|
296
|
+
if (name === needle) return 1e3 + directoryBonus;
|
|
297
|
+
if (name.startsWith(needle)) return 900 + directoryBonus;
|
|
298
|
+
if (name.includes(needle)) return 700 + directoryBonus;
|
|
299
|
+
if (path.includes(needle)) return 500 + directoryBonus;
|
|
300
|
+
const subsequence = subsequenceScore(path, needle);
|
|
301
|
+
return subsequence === void 0 ? void 0 : 300 + subsequence + directoryBonus;
|
|
302
|
+
}
|
|
303
|
+
function subsequenceScore(target, query) {
|
|
304
|
+
let targetIndex = 0;
|
|
305
|
+
let gap = 0;
|
|
306
|
+
for (const character of query) {
|
|
307
|
+
const found = target.indexOf(character, targetIndex);
|
|
308
|
+
if (found < 0) return void 0;
|
|
309
|
+
gap += found - targetIndex;
|
|
310
|
+
targetIndex = found + 1;
|
|
311
|
+
}
|
|
312
|
+
return Math.max(0, 100 - gap);
|
|
313
|
+
}
|
|
314
|
+
function kindRank(kind) {
|
|
315
|
+
return kind === "directory" ? 0 : 1;
|
|
316
|
+
}
|
|
317
|
+
function compareText(left, right) {
|
|
318
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
319
|
+
}
|
|
320
|
+
function visibleForGlobalQuery(path, query) {
|
|
321
|
+
if (query.startsWith(".") || query.includes("/.")) return true;
|
|
322
|
+
return !path.split("/").some((segment) => segment.startsWith("."));
|
|
323
|
+
}
|
|
324
|
+
function messageOf(error) {
|
|
325
|
+
return error instanceof Error ? error.message : String(error);
|
|
326
|
+
}
|
|
327
|
+
//#endregion
|
|
328
|
+
//#region src/p2/router-refs.ts
|
|
329
|
+
/**
|
|
330
|
+
* Transparent file-reference router: agent-world-keyed delegation between the
|
|
331
|
+
* stock local implementation (private realm) and per-host remote search. The
|
|
332
|
+
* stock row is prompt-complete (it installs the @ guidance section for every
|
|
333
|
+
* agent it sees), so this router only routes list() and never touches prompts.
|
|
334
|
+
*
|
|
335
|
+
* Reads are soft (ctx.get): declaring downstream services in inject would
|
|
336
|
+
* cycle startup (see the remote-vs inject comment). A missing workingDirectory
|
|
337
|
+
* degrades to the local implementation (which fails its own way if the
|
|
338
|
+
* composition is truly broken); tombstoned sessions throw fail-closed.
|
|
339
|
+
*/
|
|
340
|
+
/**
|
|
341
|
+
* Tools that only read the tree, so their result cannot have changed it.
|
|
342
|
+
*
|
|
343
|
+
* Deliberately a list of exemptions rather than a list of writers: a tool this
|
|
344
|
+
* build has never heard of keeps the stock behaviour and invalidates, which is
|
|
345
|
+
* what makes the narrowing safe — a bare query can never answer from an index
|
|
346
|
+
* that predates a possible change.
|
|
347
|
+
*/
|
|
348
|
+
const READ_ONLY_TOOLS = /* @__PURE__ */ new Set([
|
|
349
|
+
"read",
|
|
350
|
+
"read_image",
|
|
351
|
+
"glob",
|
|
352
|
+
"grep",
|
|
353
|
+
"remote_ls",
|
|
354
|
+
"remote_read"
|
|
355
|
+
]);
|
|
356
|
+
/** Ceiling for in-flight call bookkeeping; a settled call releases its slot. */
|
|
357
|
+
const PENDING_CALL_CAP = 256;
|
|
358
|
+
var TransparentFileReferences = class extends FileReferenceService {
|
|
359
|
+
static inject = ["localFileReferences", "remoteWorlds"];
|
|
360
|
+
/** Agent-scoped remote searchers (auto-released with their agents). */
|
|
361
|
+
remoteSearches = /* @__PURE__ */ new WeakMap();
|
|
362
|
+
/** `session + call id` -> tool name, for calls whose result is still to come. */
|
|
363
|
+
pendingCalls = /* @__PURE__ */ new Map();
|
|
364
|
+
constructor(ctx) {
|
|
365
|
+
super(ctx);
|
|
366
|
+
ctx.on("session/event", (session, event) => {
|
|
367
|
+
try {
|
|
368
|
+
if (event.type === "tool/call") {
|
|
369
|
+
this.rememberCall(session.id, event.data.callId, event.data.name);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
if (event.type !== "tool/result") return;
|
|
373
|
+
if (this.isReadOnlyResult(session.id, event.data.message.toolCallId)) return;
|
|
374
|
+
const agent = ctx.get("agents")?.get(session.id);
|
|
375
|
+
if (agent === void 0) return;
|
|
376
|
+
this.remoteSearches.get(agent)?.search.invalidate();
|
|
377
|
+
} catch {}
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Whether this result is positively known to come from a tree reader. A call
|
|
382
|
+
* we never saw start, an unrecognized tool, or a payload that does not even
|
|
383
|
+
* parse all answer "no": skipping the invalidation is only safe on proof, and
|
|
384
|
+
* everything else must keep the guarantee that a bare query cannot answer
|
|
385
|
+
* from an index older than the last possible change.
|
|
386
|
+
*/
|
|
387
|
+
isReadOnlyResult(sessionId, callId) {
|
|
388
|
+
try {
|
|
389
|
+
const tool = this.takeCall(sessionId, callId);
|
|
390
|
+
return tool !== void 0 && READ_ONLY_TOOLS.has(tool);
|
|
391
|
+
} catch {
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Remember which tool a call id belongs to. Keyed by session as well, because
|
|
397
|
+
* a call id is only unique within the session that issued it — two sessions
|
|
398
|
+
* (an agent and its teammate) can otherwise cross-talk and mask a write.
|
|
399
|
+
*/
|
|
400
|
+
rememberCall(sessionId, callId, tool) {
|
|
401
|
+
if (this.pendingCalls.size >= PENDING_CALL_CAP) {
|
|
402
|
+
const oldest = this.pendingCalls.keys().next().value;
|
|
403
|
+
if (oldest !== void 0) this.pendingCalls.delete(oldest);
|
|
404
|
+
}
|
|
405
|
+
this.pendingCalls.set(`${sessionId}\u0000${callId}`, tool);
|
|
406
|
+
}
|
|
407
|
+
/** Name of the tool whose result this is (consume it: results arrive once). */
|
|
408
|
+
takeCall(sessionId, callId) {
|
|
409
|
+
const key = `${sessionId}\u0000${callId}`;
|
|
410
|
+
const tool = this.pendingCalls.get(key);
|
|
411
|
+
this.pendingCalls.delete(key);
|
|
412
|
+
return tool;
|
|
413
|
+
}
|
|
414
|
+
get local() {
|
|
415
|
+
return this.ctx.localFileReferences;
|
|
416
|
+
}
|
|
417
|
+
get remote() {
|
|
418
|
+
return this.ctx.remoteWorlds;
|
|
419
|
+
}
|
|
420
|
+
effective(route, what) {
|
|
421
|
+
const mode = this.remote.mode;
|
|
422
|
+
if (mode === "off") return { kind: "local" };
|
|
423
|
+
if (mode === "shadow") {
|
|
424
|
+
this.remote.note(route, what);
|
|
425
|
+
return { kind: "local" };
|
|
426
|
+
}
|
|
427
|
+
return route;
|
|
428
|
+
}
|
|
429
|
+
async list(agent, query, signal) {
|
|
430
|
+
const workingDirectory = this.ctx.get("workingDirectory");
|
|
431
|
+
if (workingDirectory === void 0) return this.local.list(agent, query, signal);
|
|
432
|
+
let cwd;
|
|
433
|
+
try {
|
|
434
|
+
cwd = workingDirectory.get(agent.session);
|
|
435
|
+
} catch {
|
|
436
|
+
return this.local.list(agent, query, signal);
|
|
437
|
+
}
|
|
438
|
+
const route = this.effective(this.remote.routeDir(cwd), `@refs @ ${cwd}`);
|
|
439
|
+
if (route.kind === "local") return this.local.list(agent, query, signal);
|
|
440
|
+
return (await this.searchFor(agent, route)).list(query, signal);
|
|
441
|
+
}
|
|
442
|
+
/** Per-agent remote searcher, rebuilt when the binding changes. */
|
|
443
|
+
async searchFor(agent, route) {
|
|
444
|
+
const cached = this.remoteSearches.get(agent);
|
|
445
|
+
if (cached !== void 0 && cached.routeId === route.id) return cached.search;
|
|
446
|
+
cached?.search.dispose();
|
|
447
|
+
const runtime = this.remote.procFor(route.alias);
|
|
448
|
+
const search = new RemoteFileSearch({
|
|
449
|
+
fs: await this.remote.fsFor(route.alias),
|
|
450
|
+
spawn: (spec) => runtime.spawn(spec),
|
|
451
|
+
remoteRoot: route.remotePath,
|
|
452
|
+
warn: (message) => {
|
|
453
|
+
try {
|
|
454
|
+
this.ctx.logger.warn(message);
|
|
455
|
+
} catch {}
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
this.remoteSearches.set(agent, {
|
|
459
|
+
routeId: route.id,
|
|
460
|
+
search
|
|
461
|
+
});
|
|
462
|
+
return search;
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
//#endregion
|
|
466
|
+
export { TransparentFileReferences as default };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { SpillLocator, SpillStore } from "@deepseek-ai/dsh-spill";
|
|
3
|
+
//#region src/p2/router-spill.ts
|
|
4
|
+
/**
|
|
5
|
+
* Transparent spill router: session-world-keyed delegation between the local
|
|
6
|
+
* spill backend (private realm) and per-host remote spill dirs. Unknown
|
|
7
|
+
* sessions stay local (system callers); tombstoned sessions fail closed and
|
|
8
|
+
* the caller keeps its inline result.
|
|
9
|
+
*/
|
|
10
|
+
/** Sanitize a suggested name to one safe segment (backend owns the rest). */
|
|
11
|
+
function safeName(suggested) {
|
|
12
|
+
const base = suggested.replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 64);
|
|
13
|
+
return `${base === "" ? "spill" : base}-${randomUUID().slice(0, 8)}.txt`;
|
|
14
|
+
}
|
|
15
|
+
var TransparentSpillStore = class extends SpillStore {
|
|
16
|
+
static inject = ["localSpillStore", "remoteWorlds"];
|
|
17
|
+
get local() {
|
|
18
|
+
return this.ctx.localSpillStore;
|
|
19
|
+
}
|
|
20
|
+
get remote() {
|
|
21
|
+
return this.ctx.remoteWorlds;
|
|
22
|
+
}
|
|
23
|
+
effective(route, what) {
|
|
24
|
+
const mode = this.remote.mode;
|
|
25
|
+
if (mode === "off") return { kind: "local" };
|
|
26
|
+
if (mode === "shadow") {
|
|
27
|
+
this.remote.note(route, what);
|
|
28
|
+
return { kind: "local" };
|
|
29
|
+
}
|
|
30
|
+
return route;
|
|
31
|
+
}
|
|
32
|
+
async saveText(input) {
|
|
33
|
+
const route = this.effective(this.remote.sessionWorld(input.owner.sessionId), `spill ${input.suggestedName ?? ""}`);
|
|
34
|
+
if (route.kind === "local") return this.local.saveText(input);
|
|
35
|
+
const fs = await this.remote.fsFor(route.alias);
|
|
36
|
+
const dir = `${await this.remote.homeFor(route.alias)}/.dsh-helpers/spills/session-${input.owner.sessionId}`;
|
|
37
|
+
await this.remote.mkdirRemote(route.alias, dir);
|
|
38
|
+
const fullPath = `${dir}/${safeName(input.suggestedName ?? "spill.txt")}`;
|
|
39
|
+
const target = await fs.resolve(fullPath, {});
|
|
40
|
+
await fs.writeText(target, input.content, { kind: "createIfAbsent" }, void 0, this.policyFor(dir));
|
|
41
|
+
const bytes = Buffer.byteLength(input.content, "utf8");
|
|
42
|
+
return {
|
|
43
|
+
locator: SpillLocator(`remote:${route.alias}:${fullPath}`),
|
|
44
|
+
bytes,
|
|
45
|
+
retrievalHint: `remote spill on ${route.alias} (${bytes} bytes); read ${fullPath} with the read tool in this session`
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** Explicit remote policy: mirrored mode, spill dir as the fence root. */
|
|
49
|
+
policyFor(dir) {
|
|
50
|
+
let mode = "danger-full-access";
|
|
51
|
+
try {
|
|
52
|
+
const resolved = this.ctx.get("sandboxPolicy")?.resolve?.()?.mode;
|
|
53
|
+
if (resolved === "read-only" || resolved === "workspace-write" || resolved === "danger-full-access") mode = resolved;
|
|
54
|
+
} catch {}
|
|
55
|
+
return {
|
|
56
|
+
mode,
|
|
57
|
+
workspaceRoot: dir
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
//#endregion
|
|
62
|
+
export { TransparentSpillStore as default };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { n as remoteCommand } from "./ssh-proc-arI5lm4W.js";
|
|
2
|
+
import { SubprocessExecutableNotFoundError, SubprocessRuntime } from "@deepseek-ai/dsh-subprocess";
|
|
3
|
+
//#region src/p2/router-subprocess.ts
|
|
4
|
+
/**
|
|
5
|
+
* Transparent subprocess router: cwd-keyed delegation between the local
|
|
6
|
+
* runtime (private realm) and per-host ssh runtimes. Handles are created by
|
|
7
|
+
* the chosen backend, so no handle mapping is needed; pwsh argv in a remote
|
|
8
|
+
* world fails closed with guidance instead of exit-127 confusion.
|
|
9
|
+
*/
|
|
10
|
+
/** True when argv selects PowerShell (remote worlds are POSIX-only). */
|
|
11
|
+
function isPwshArgv(argv) {
|
|
12
|
+
if (argv.length === 0) return false;
|
|
13
|
+
const base = argv[0].replace(/\\/g, "/").split("/").pop() ?? "";
|
|
14
|
+
return /^pwsh(\.exe)?$/i.test(base);
|
|
15
|
+
}
|
|
16
|
+
var TransparentSubprocessRuntime = class extends SubprocessRuntime {
|
|
17
|
+
static inject = ["localSubprocess", "remoteWorlds"];
|
|
18
|
+
get local() {
|
|
19
|
+
return this.ctx.localSubprocess;
|
|
20
|
+
}
|
|
21
|
+
get remote() {
|
|
22
|
+
return this.ctx.remoteWorlds;
|
|
23
|
+
}
|
|
24
|
+
effective(route, what) {
|
|
25
|
+
const mode = this.remote.mode;
|
|
26
|
+
if (mode === "off") return { kind: "local" };
|
|
27
|
+
if (mode === "shadow") {
|
|
28
|
+
this.remote.note(route, what);
|
|
29
|
+
return { kind: "local" };
|
|
30
|
+
}
|
|
31
|
+
return route;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Executable lookup stays local (the seam carries no cwd): a remote spawn
|
|
35
|
+
* re-resolves in its own world. Mirrors the Yan-Zero rule.
|
|
36
|
+
*/
|
|
37
|
+
async resolveExecutable(command, env, signal) {
|
|
38
|
+
return this.local.resolveExecutable(command, env, signal);
|
|
39
|
+
}
|
|
40
|
+
/** Provider environment facts stay local (used by local UI shell selection). */
|
|
41
|
+
async terminalEnvironment(signal) {
|
|
42
|
+
return this.local.terminalEnvironment(signal);
|
|
43
|
+
}
|
|
44
|
+
spawn(spec) {
|
|
45
|
+
const route = this.effective(this.remote.routeDir(spec.cwd), `spawn ${spec.argv[0] ?? ""} @ ${spec.cwd}`);
|
|
46
|
+
if (route.kind === "local") return this.local.spawn(spec);
|
|
47
|
+
if (isPwshArgv(spec.argv)) throw new SubprocessExecutableNotFoundError(`remote worlds are POSIX-only: '${spec.argv[0]}' cannot run on ${route.alias}; use the bash tool (Remote preset) instead`);
|
|
48
|
+
const runtime = this.remote.procFor(route.alias);
|
|
49
|
+
const argv0 = spec.argv[0];
|
|
50
|
+
const remoteArgv0 = argv0.startsWith("/") ? argv0 : /^[A-Za-z]:[\\/]/.test(argv0) || argv0.includes("\\") ? remoteCommand(argv0) : argv0;
|
|
51
|
+
const cwd = this.remote.toRemotePath(route, spec.cwd);
|
|
52
|
+
return runtime.spawn({
|
|
53
|
+
...spec,
|
|
54
|
+
argv: [remoteArgv0, ...spec.argv.slice(1)],
|
|
55
|
+
cwd
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
async spawnTerminal(spec) {
|
|
59
|
+
if (this.effective(this.remote.routeDir(spec.cwd), `spawnTerminal @ ${spec.cwd}`).kind === "local") return this.local.spawnTerminal(spec);
|
|
60
|
+
throw new Error("remote agent terminals are not implemented in P2-v1 (interactive remote shells live in the Remote tab); file/shell/subprocess routing is unaffected");
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
//#endregion
|
|
64
|
+
export { TransparentSubprocessRuntime as default };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { LocalBashExecutor } from "@deepseek-ai/dsh-bash-local";
|
|
2
|
+
//#region src/p2/shell-bash.ts
|
|
3
|
+
/**
|
|
4
|
+
* Routed bash executor: the stock local bash executor with world-aware
|
|
5
|
+
* dispatch. Local worlds run unchanged; remote worlds strip host-local
|
|
6
|
+
* DSH_* facts and spawn through the (routed) subprocess service, which
|
|
7
|
+
* carries the call to the right host by workdir.
|
|
8
|
+
*
|
|
9
|
+
* Mounted inside preset-remote's isolated shell group alongside tool-bash;
|
|
10
|
+
* the stock root shell rows are untouched.
|
|
11
|
+
*/
|
|
12
|
+
var RoutedBashExecutor = class extends LocalBashExecutor {
|
|
13
|
+
static inject = ["subprocess", "remoteWorlds"];
|
|
14
|
+
get remote() {
|
|
15
|
+
return this.ctx.remoteWorlds;
|
|
16
|
+
}
|
|
17
|
+
async execute(spec) {
|
|
18
|
+
if (this.remote.routeDir(spec.workdir).kind === "local") return super.execute(spec);
|
|
19
|
+
const { dshEnv: _dropped, ...rest } = spec;
|
|
20
|
+
return super.execute(rest);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
//#endregion
|
|
24
|
+
export { RoutedBashExecutor as default };
|