@tiphareth/dsh-hardssh 0.1.2-alpha → 0.2.2
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 +30 -0
- package/README.md +49 -25
- package/SKILLS.md +140 -0
- package/lib/base/index.js +95 -0
- package/lib/client.js +1165 -1329
- package/lib/fs.js +307 -416
- package/lib/index.js +5428 -2991
- package/lib/ledger-B-LXlftp.js +43 -0
- package/lib/ledger-D2ezq1iW.js +376 -0
- package/lib/model-Cp7f70Mb.js +5 -0
- package/lib/registry-CViuzKYI.js +421 -0
- package/lib/subprocess.js +92 -549
- package/lib/types/backend.d.ts +73 -78
- package/lib/types/base/capability.d.ts +4 -0
- package/lib/types/base/ledger-router.d.ts +42 -26
- package/lib/types/base/ledger.d.ts +30 -19
- package/lib/types/base/model.d.ts +17 -8
- package/lib/types/base/plugin.d.ts +11 -0
- package/lib/types/base/registry.d.ts +28 -31
- package/lib/types/base/router.d.ts +0 -7
- package/lib/types/client/api.d.ts +8 -8
- package/lib/types/client/connect-host.d.ts +13 -0
- package/lib/types/client/index.d.ts +6 -4
- package/lib/types/client/locales.d.ts +15 -1
- package/lib/types/client/session-connect-gate.d.ts +71 -0
- package/lib/types/client/ssh/api.d.ts +11 -9
- package/lib/types/client/ssh/apply.d.ts +14 -8
- package/lib/types/client/ssh/locales.d.ts +8 -1
- package/lib/types/client/ssh/ops-tab.d.ts +34 -0
- package/lib/types/client/ssh/panel/ClusterTab.d.ts +3 -1
- package/lib/types/client/ssh/panel/ConnectionErrorDialog.d.ts +8 -0
- package/lib/types/client/ssh/panel/SessionSecretDialog.d.ts +4 -1
- package/lib/types/client/ssh/panel/SshPanel.d.ts +8 -8
- package/lib/types/client/ssh/panel/TerminalTab.d.ts +3 -5
- package/lib/types/client/ssh/panel/TransferTab.d.ts +5 -1
- package/lib/types/client/ssh/panel/TunnelsTab.d.ts +3 -1
- package/lib/types/client/ssh/session-target.d.ts +17 -0
- package/lib/types/client/state.d.ts +4 -1
- package/lib/types/client/workspace-panel-entry.d.ts +30 -0
- package/lib/types/client/workspace-panel.d.ts +24 -0
- package/lib/types/client-http.d.ts +77 -6
- package/lib/types/core.d.ts +5 -16
- package/lib/types/fs.d.ts +18 -25
- package/lib/types/index.d.ts +138 -9
- package/lib/types/ledger.d.ts +16 -111
- package/lib/types/protocol.d.ts +0 -5
- package/lib/types/providers/index.d.ts +6 -3
- package/lib/types/providers/local/provider.d.ts +83 -23
- package/lib/types/providers/ssh/provider.d.ts +24 -57
- package/lib/types/remote/environment.d.ts +2 -0
- package/lib/types/remote/remote-fs.d.ts +12 -1
- package/lib/types/remote/remote-process.d.ts +22 -1
- package/lib/types/remote/remote-subprocess.d.ts +13 -1
- package/lib/types/remote/remote-terminal.d.ts +4 -0
- package/lib/types/remote-search.d.ts +3 -3
- package/lib/types/routes.d.ts +36 -8
- package/lib/types/runtime/dsh-capabilities.d.ts +18 -0
- package/lib/types/runtime/workspace-core.d.ts +73 -38
- package/lib/types/runtime/workspace-migration.d.ts +113 -0
- package/lib/types/ssh/connection/lease.d.ts +23 -0
- package/lib/types/ssh/connection/manager.d.ts +359 -0
- package/lib/types/ssh/connection/pool.d.ts +8 -1
- package/lib/types/ssh/engine.d.ts +97 -267
- package/lib/types/ssh/known-hosts.d.ts +22 -5
- package/lib/types/ssh/local-transfer-policy.d.ts +20 -0
- package/lib/types/ssh/plugin.d.ts +10 -15
- package/lib/types/ssh/protocol.d.ts +18 -0
- package/lib/types/ssh/routes.d.ts +15 -7
- package/lib/types/ssh/sftp/service.d.ts +216 -0
- package/lib/types/ssh/store.d.ts +20 -8
- package/lib/types/ssh/terminal/service.d.ts +49 -0
- package/lib/types/ssh/tunnel/service.d.ts +26 -0
- package/lib/types/ssh/vault.d.ts +51 -4
- package/lib/types/subprocess.d.ts +13 -21
- package/lib/types/switch/switch-fs.d.ts +52 -2
- package/lib/types/switch/switch-subprocess.d.ts +32 -3
- package/lib/types/tools.d.ts +7 -6
- package/lib/types/workspace-tool-ops.d.ts +33 -0
- package/lib/types/workspace.d.ts +18 -0
- package/lib/vault-3gpWct2Q.js +559 -0
- package/lib/workspace.js +2 -0
- package/package.json +36 -11
- package/scripts/export-legacy-workspaces.mjs +136 -0
- package/src/backend.ts +155 -483
- package/src/base/capability.ts +4 -0
- package/src/base/ledger-router.ts +214 -65
- package/src/base/ledger.ts +175 -48
- package/src/base/model.ts +18 -8
- package/src/base/plugin.ts +15 -4
- package/src/base/registry.ts +49 -41
- package/src/base/router.ts +0 -8
- package/src/client/api.ts +8 -36
- package/src/client/connect-host.ts +199 -0
- package/src/client/directory-flow.tsx +91 -36
- package/src/client/index.ts +56 -53
- package/src/client/locales.ts +14 -0
- package/src/client/session-connect-gate.ts +163 -0
- package/src/client/ssh/api.ts +245 -108
- package/src/client/ssh/apply.ts +23 -24
- package/src/client/ssh/locales.ts +16 -2
- package/src/client/ssh/ops-tab.tsx +79 -0
- package/src/client/ssh/panel/ClusterTab.tsx +9 -20
- package/src/client/ssh/panel/ConnectionErrorDialog.tsx +33 -0
- package/src/client/ssh/panel/SessionSecretDialog.tsx +5 -1
- package/src/client/ssh/panel/SshPanel.tsx +81 -77
- package/src/client/ssh/panel/TerminalTab.tsx +18 -37
- package/src/client/ssh/panel/TransferTab.tsx +43 -33
- package/src/client/ssh/panel/TunnelsTab.tsx +9 -26
- package/src/client/ssh/panel/panel.module.css +90 -127
- package/src/client/ssh/session-target.ts +69 -0
- package/src/client/state.ts +9 -1
- package/src/client/workspace-badges.ts +99 -99
- package/src/client/workspace-panel-entry.tsx +91 -0
- package/src/client/workspace-panel.tsx +227 -0
- package/src/client/workspace.module.css +76 -2
- package/src/client-http.ts +118 -18
- package/src/core.ts +37 -47
- package/src/fs.ts +137 -85
- package/src/index.ts +535 -244
- package/src/ledger.ts +62 -416
- package/src/protocol.ts +0 -6
- package/src/providers/index.ts +8 -4
- package/src/providers/local/provider.ts +259 -87
- package/src/providers/ssh/provider.ts +158 -155
- package/src/remote/environment.ts +22 -1
- package/src/remote/remote-fs.ts +123 -34
- package/src/remote/remote-process.ts +265 -203
- package/src/remote/remote-subprocess.ts +61 -18
- package/src/remote/remote-terminal.ts +106 -21
- package/src/remote-search.ts +122 -26
- package/src/routes.ts +416 -395
- package/src/runtime/dsh-capabilities.ts +19 -0
- package/src/runtime/workspace-core.ts +171 -88
- package/src/runtime/workspace-migration.ts +472 -0
- package/src/ssh/connection/lease.ts +35 -0
- package/src/ssh/connection/manager.ts +1083 -0
- package/src/ssh/connection/pool.ts +341 -275
- package/src/ssh/engine.ts +269 -1477
- package/src/ssh/known-hosts.ts +42 -18
- package/src/ssh/local-transfer-policy.ts +83 -0
- package/src/ssh/plugin.ts +20 -19
- package/src/ssh/protocol.ts +22 -1
- package/src/ssh/routes.ts +419 -187
- package/src/ssh/sftp/service.ts +967 -0
- package/src/ssh/store.ts +169 -72
- package/src/ssh/terminal/service.ts +177 -0
- package/src/ssh/tools.ts +42 -20
- package/src/ssh/tunnel/service.ts +217 -0
- package/src/ssh/vault.ts +245 -91
- package/src/subprocess.ts +88 -71
- package/src/switch/switch-fs.ts +141 -10
- package/src/switch/switch-subprocess.ts +80 -6
- package/src/tools.ts +217 -221
- package/src/workspace-tool-ops.ts +101 -0
- package/src/workspace.ts +52 -0
- package/lib/environment-BL1jddfB.js +0 -449
- package/lib/switch-fs-CAJpFY9C.js +0 -193
- package/lib/switch-fs-RrZtG2gv.js +0 -210
- package/lib/types/client/manager-button.d.ts +0 -32
- package/lib/types/client/ssh/mount.d.ts +0 -13
- package/lib/types/client/ssh/panel/HostsTab.d.ts +0 -10
- package/lib/types/client/ssh/panel/controller.d.ts +0 -23
- package/lib/types/client/ssh/sidebar-entry.d.ts +0 -25
- package/lib/types/client/workspace-gate.d.ts +0 -15
- package/lib/types/remote-runner.d.ts +0 -83
- package/lib/types/seam-state.d.ts +0 -69
- package/src/client/manager-button.tsx +0 -269
- package/src/client/ssh/mount.tsx +0 -83
- package/src/client/ssh/panel/HostsTab.tsx +0 -236
- package/src/client/ssh/panel/controller.ts +0 -48
- package/src/client/ssh/sidebar-entry.ts +0 -123
- package/src/client/workspace-gate.ts +0 -232
- package/src/remote-runner.ts +0 -201
- package/src/seam-state.ts +0 -185
package/lib/subprocess.js
CHANGED
|
@@ -1,533 +1,7 @@
|
|
|
1
|
-
import { i as
|
|
2
|
-
import {
|
|
3
|
-
import { closeSync, mkdtempSync, openSync, unlinkSync, writeSync } from "node:fs";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import { join, posix } from "node:path";
|
|
1
|
+
import { i as isPathUnderAnchor } from "./ledger-D2ezq1iW.js";
|
|
2
|
+
import { t as anchorRoot } from "./ledger-B-LXlftp.js";
|
|
6
3
|
import { SubprocessRuntime } from "@deepseek-ai/dsh-subprocess";
|
|
7
4
|
import { LocalSubprocessRuntime } from "@deepseek-ai/dsh-subprocess-local";
|
|
8
|
-
import { MAX_TIMER_DELAY_MS } from "@deepseek-ai/dsh-timeout";
|
|
9
|
-
import { PassThrough } from "node:stream";
|
|
10
|
-
//#region src/remote/output.ts
|
|
11
|
-
/**
|
|
12
|
-
* Bounded host-side projection of one remote output stream with a local
|
|
13
|
-
* spill file. Ported from UynajGI/dsh-ssh (MIT) — verbatim semantics.
|
|
14
|
-
*/
|
|
15
|
-
let spillCounter = 0;
|
|
16
|
-
let defaultSpillDir;
|
|
17
|
-
/** Private (0700) per-process spill directory under the OS tmpdir, created lazily. */
|
|
18
|
-
function privateSpillDir() {
|
|
19
|
-
defaultSpillDir ??= mkdtempSync(join(tmpdir(), "dsh-subprocess-ssh-"));
|
|
20
|
-
return defaultSpillDir;
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Collects one remote stream with a bounded in-memory tail. On first overflow
|
|
24
|
-
* a spill file is opened (when a spill cap is configured) and every chunk —
|
|
25
|
-
* already-collected ones included — is appended there while the full stream
|
|
26
|
-
* stays within the cap; without one, only the in-memory tail is retained.
|
|
27
|
-
*/
|
|
28
|
-
var SshOutputCollector = class {
|
|
29
|
-
maxBytes;
|
|
30
|
-
maxSpillBytes;
|
|
31
|
-
label;
|
|
32
|
-
spillDir;
|
|
33
|
-
chunks = [];
|
|
34
|
-
retained = 0;
|
|
35
|
-
total = 0;
|
|
36
|
-
dropped = false;
|
|
37
|
-
spillFd;
|
|
38
|
-
spillFile;
|
|
39
|
-
spillDisabled;
|
|
40
|
-
constructor(maxBytes, maxSpillBytes, label, spillDir = privateSpillDir()) {
|
|
41
|
-
this.maxBytes = maxBytes;
|
|
42
|
-
this.maxSpillBytes = maxSpillBytes;
|
|
43
|
-
this.label = label;
|
|
44
|
-
this.spillDir = spillDir;
|
|
45
|
-
this.spillDisabled = maxSpillBytes === void 0;
|
|
46
|
-
}
|
|
47
|
-
/** Append one byte-faithful remote chunk, trimming the retained tail to the cap. */
|
|
48
|
-
push(chunk) {
|
|
49
|
-
if (chunk.length === 0) return;
|
|
50
|
-
const buffer = Buffer.from(chunk);
|
|
51
|
-
this.total += buffer.length;
|
|
52
|
-
const overflows = this.retained + buffer.length > this.maxBytes;
|
|
53
|
-
if (!this.spillDisabled && (overflows || this.spillFd !== void 0)) this.spillAll(buffer);
|
|
54
|
-
this.chunks.push(buffer);
|
|
55
|
-
this.retained += buffer.length;
|
|
56
|
-
while (this.retained > this.maxBytes) {
|
|
57
|
-
const head = this.chunks[0];
|
|
58
|
-
const excess = this.retained - this.maxBytes;
|
|
59
|
-
if (head.length <= excess) {
|
|
60
|
-
this.chunks.shift();
|
|
61
|
-
this.retained -= head.length;
|
|
62
|
-
} else {
|
|
63
|
-
this.chunks[0] = head.subarray(excess);
|
|
64
|
-
this.retained -= excess;
|
|
65
|
-
}
|
|
66
|
-
this.dropped = true;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
/** @inheritdoc */
|
|
70
|
-
readFrom(fromByte) {
|
|
71
|
-
const retained = Buffer.concat(this.chunks, this.retained);
|
|
72
|
-
const windowStart = this.total - this.retained;
|
|
73
|
-
const lossy = fromByte < windowStart;
|
|
74
|
-
const start = lossy ? 0 : Math.min(retained.length, Math.max(0, fromByte - windowStart));
|
|
75
|
-
return {
|
|
76
|
-
text: retained.subarray(start).toString("utf8"),
|
|
77
|
-
nextOffset: this.total,
|
|
78
|
-
lossy,
|
|
79
|
-
...this.spillFile !== void 0 ? { spillPath: this.spillFile } : {}
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
/** Open the spill file lazily and append one chunk (plus any prior chunks once). */
|
|
83
|
-
spillAll(chunk) {
|
|
84
|
-
if (this.maxSpillBytes !== void 0 && this.total > this.maxSpillBytes) {
|
|
85
|
-
this.discardSpill();
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
if (this.spillFd === void 0) {
|
|
89
|
-
this.spillFile = join(this.spillDir, `dsh-subprocess-ssh-${process.pid}-${++spillCounter}-${randomBytes(6).toString("hex")}-${this.label}.log`);
|
|
90
|
-
this.spillFd = openSync(this.spillFile, "wx", 384);
|
|
91
|
-
for (const prior of this.chunks) writeSync(this.spillFd, prior);
|
|
92
|
-
}
|
|
93
|
-
writeSync(this.spillFd, chunk);
|
|
94
|
-
}
|
|
95
|
-
/** Stop spilling and remove the file once it can no longer hold the complete stream. */
|
|
96
|
-
discardSpill() {
|
|
97
|
-
const fd = this.spillFd;
|
|
98
|
-
const file = this.spillFile;
|
|
99
|
-
this.spillFd = void 0;
|
|
100
|
-
this.spillFile = void 0;
|
|
101
|
-
this.spillDisabled = true;
|
|
102
|
-
if (fd !== void 0) try {
|
|
103
|
-
closeSync(fd);
|
|
104
|
-
} catch {
|
|
105
|
-
this.spillFd = fd;
|
|
106
|
-
}
|
|
107
|
-
if (file !== void 0) try {
|
|
108
|
-
unlinkSync(file);
|
|
109
|
-
} catch {}
|
|
110
|
-
}
|
|
111
|
-
/** Close the spill file once the stream has ended; stop advertising it on a failed close. */
|
|
112
|
-
seal() {
|
|
113
|
-
if (this.spillFd === void 0) return;
|
|
114
|
-
try {
|
|
115
|
-
closeSync(this.spillFd);
|
|
116
|
-
} catch {
|
|
117
|
-
this.spillFile = void 0;
|
|
118
|
-
}
|
|
119
|
-
this.spillFd = void 0;
|
|
120
|
-
}
|
|
121
|
-
/** Seal the spill and return the final collected output. */
|
|
122
|
-
finalize() {
|
|
123
|
-
this.seal();
|
|
124
|
-
return {
|
|
125
|
-
text: Buffer.concat(this.chunks).toString("utf8"),
|
|
126
|
-
truncated: this.dropped,
|
|
127
|
-
...this.spillFile !== void 0 ? { spillPath: this.spillFile } : {}
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
};
|
|
131
|
-
//#endregion
|
|
132
|
-
//#region src/remote/remote-process.ts
|
|
133
|
-
/**
|
|
134
|
-
* One asynchronously-started SSH command projected onto the subprocess seam.
|
|
135
|
-
* Ported from UynajGI/dsh-ssh (MIT) — the raw ssh2 channel is replaced by the
|
|
136
|
-
* engine's streaming ExecSession.
|
|
137
|
-
*/
|
|
138
|
-
function isCollect(mode) {
|
|
139
|
-
return mode !== "pipe" && mode !== "inherit";
|
|
140
|
-
}
|
|
141
|
-
/** Resolve the remote working directory for one spawn spec. */
|
|
142
|
-
function resolveRemoteCwd(state, cwd) {
|
|
143
|
-
const root = state.remoteRoot;
|
|
144
|
-
if (root === void 0) throw new Error("subprocess-ssh: remote workspace root is not set");
|
|
145
|
-
if (cwd !== void 0 && cwd.startsWith("/")) return cwd;
|
|
146
|
-
return root;
|
|
147
|
-
}
|
|
148
|
-
/** Build the remote command text: cd, env -i scrub, exec the argv. */
|
|
149
|
-
async function buildCommand(engine, alias, state, spec) {
|
|
150
|
-
const environment = serializeEnvironment(await readScrubbedRemoteEnvironment(engine, alias), spec.env);
|
|
151
|
-
const argv = spec.argv.map(shellQuote).join(" ");
|
|
152
|
-
return `cd -- ${shellQuote(resolveRemoteCwd(state, spec.cwd))} && exec env -i -- ${environment} ${argv}`;
|
|
153
|
-
}
|
|
154
|
-
/** SSH-backed subprocess handle. The channel does not expose a remote pid, so `pid` is `-1`. */
|
|
155
|
-
var SshSubprocessHandle = class {
|
|
156
|
-
engine;
|
|
157
|
-
getState;
|
|
158
|
-
spec;
|
|
159
|
-
spillDir;
|
|
160
|
-
stdin;
|
|
161
|
-
stdout;
|
|
162
|
-
stderr;
|
|
163
|
-
collected;
|
|
164
|
-
done;
|
|
165
|
-
terminationController = new AbortController();
|
|
166
|
-
stdoutCollector;
|
|
167
|
-
stderrCollector;
|
|
168
|
-
session;
|
|
169
|
-
graceTimer;
|
|
170
|
-
settled = false;
|
|
171
|
-
constructor(engine, getState, spec, spillDir) {
|
|
172
|
-
this.engine = engine;
|
|
173
|
-
this.getState = getState;
|
|
174
|
-
this.spec = spec;
|
|
175
|
-
this.spillDir = spillDir;
|
|
176
|
-
const outMode = spec.stdio.stdout;
|
|
177
|
-
const errMode = spec.stdio.stderr;
|
|
178
|
-
this.stdout = outMode === "pipe" ? new PassThrough() : void 0;
|
|
179
|
-
this.stderr = errMode === "pipe" ? new PassThrough() : void 0;
|
|
180
|
-
this.stdoutCollector = isCollect(outMode) ? new SshOutputCollector(outMode.maxBytes, outMode.spill?.maxBytes, "stdout", spillDir) : void 0;
|
|
181
|
-
this.stderrCollector = isCollect(errMode) ? new SshOutputCollector(errMode.maxBytes, errMode.spill?.maxBytes, "stderr", spillDir) : void 0;
|
|
182
|
-
this.collected = {
|
|
183
|
-
...this.stdoutCollector !== void 0 ? { stdout: this.stdoutCollector } : {},
|
|
184
|
-
...this.stderrCollector !== void 0 ? { stderr: this.stderrCollector } : {}
|
|
185
|
-
};
|
|
186
|
-
this.stdin = spec.stdio.stdin === "pipe" ? new PassThrough() : void 0;
|
|
187
|
-
spec.signal?.addEventListener("abort", this.onAbort, { once: true });
|
|
188
|
-
this.done = this.run();
|
|
189
|
-
this.done.catch(() => {});
|
|
190
|
-
if (spec.signal?.aborted === true) this.terminate();
|
|
191
|
-
}
|
|
192
|
-
/** Remote process id; `-1` because the SSH channel does not expose one. */
|
|
193
|
-
get pid() {
|
|
194
|
-
return -1;
|
|
195
|
-
}
|
|
196
|
-
/** @inheritdoc */
|
|
197
|
-
terminate() {
|
|
198
|
-
if (this.settled || this.terminationController.signal.aborted) return;
|
|
199
|
-
this.terminationController.abort(/* @__PURE__ */ new Error("subprocess-ssh: command terminated"));
|
|
200
|
-
const session = this.session;
|
|
201
|
-
if (session !== void 0) this.signalTerm(session);
|
|
202
|
-
}
|
|
203
|
-
/** @inheritdoc */
|
|
204
|
-
waitForExit(signal) {
|
|
205
|
-
if (this.settled) return Promise.resolve(true);
|
|
206
|
-
if (signal?.aborted === true) return Promise.resolve(false);
|
|
207
|
-
if (signal === void 0) return this.done.then(() => true, () => true);
|
|
208
|
-
return new Promise((resolve) => {
|
|
209
|
-
const onAbort = () => {
|
|
210
|
-
cleanup();
|
|
211
|
-
resolve(false);
|
|
212
|
-
};
|
|
213
|
-
const cleanup = () => {
|
|
214
|
-
signal.removeEventListener("abort", onAbort);
|
|
215
|
-
};
|
|
216
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
217
|
-
this.done.then(() => {
|
|
218
|
-
cleanup();
|
|
219
|
-
resolve(true);
|
|
220
|
-
}, () => {
|
|
221
|
-
cleanup();
|
|
222
|
-
resolve(true);
|
|
223
|
-
});
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
onAbort = () => {
|
|
227
|
-
this.terminate();
|
|
228
|
-
};
|
|
229
|
-
signalTerm(session) {
|
|
230
|
-
try {
|
|
231
|
-
session.signal("TERM");
|
|
232
|
-
} catch {}
|
|
233
|
-
this.graceTimer = setTimeout(() => {
|
|
234
|
-
try {
|
|
235
|
-
session.signal("KILL");
|
|
236
|
-
} catch {}
|
|
237
|
-
}, this.spec.graceMs);
|
|
238
|
-
}
|
|
239
|
-
settle() {
|
|
240
|
-
if (this.settled) return;
|
|
241
|
-
this.settled = true;
|
|
242
|
-
if (this.graceTimer !== void 0) clearTimeout(this.graceTimer);
|
|
243
|
-
this.graceTimer = void 0;
|
|
244
|
-
this.stdoutCollector?.seal();
|
|
245
|
-
this.stderrCollector?.seal();
|
|
246
|
-
this.spec.signal?.removeEventListener("abort", this.onAbort);
|
|
247
|
-
}
|
|
248
|
-
async run() {
|
|
249
|
-
let session;
|
|
250
|
-
try {
|
|
251
|
-
const state = this.getState();
|
|
252
|
-
if (state.mode !== "remote" || state.alias === void 0) throw new Error("subprocess-ssh: not in remote mode — switch the GUI to SSH mode first");
|
|
253
|
-
const command = await buildCommand(this.engine, state.alias, state, this.spec);
|
|
254
|
-
session = await this.engine.openExec(state.alias, command);
|
|
255
|
-
} catch (error) {
|
|
256
|
-
this.settle();
|
|
257
|
-
throw error;
|
|
258
|
-
}
|
|
259
|
-
this.session = session;
|
|
260
|
-
if (this.terminationController.signal.aborted) this.signalTerm(session);
|
|
261
|
-
this.wireStdout(session);
|
|
262
|
-
this.wireStderr(session);
|
|
263
|
-
if (this.stdin !== void 0) this.stdin.pipe({
|
|
264
|
-
write: (chunk, encoding, callback) => {
|
|
265
|
-
try {
|
|
266
|
-
session.send(chunk.toString(encoding ?? "utf8"));
|
|
267
|
-
callback?.();
|
|
268
|
-
} catch (error) {
|
|
269
|
-
callback?.(error instanceof Error ? error : new Error(String(error)));
|
|
270
|
-
}
|
|
271
|
-
},
|
|
272
|
-
end: (callback) => {
|
|
273
|
-
session.end();
|
|
274
|
-
callback?.();
|
|
275
|
-
}
|
|
276
|
-
});
|
|
277
|
-
else if (typeof this.spec.stdio.stdin === "object") session.end(this.spec.stdio.stdin.data);
|
|
278
|
-
return await new Promise((resolve, reject) => {
|
|
279
|
-
session.onExit = (code, error) => {
|
|
280
|
-
this.settle();
|
|
281
|
-
if (error !== void 0) reject(/* @__PURE__ */ new Error(`subprocess-ssh: ${error}`));
|
|
282
|
-
else resolve({
|
|
283
|
-
exitCode: code,
|
|
284
|
-
signal: null
|
|
285
|
-
});
|
|
286
|
-
};
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
wireStdout(session) {
|
|
290
|
-
const mode = this.spec.stdio.stdout;
|
|
291
|
-
session.onData = (data) => {
|
|
292
|
-
if (mode === "pipe") this.stdout?.write(data);
|
|
293
|
-
else if (mode === "inherit") process.stdout.write(data);
|
|
294
|
-
else this.stdoutCollector?.push(data);
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
wireStderr(session) {
|
|
298
|
-
const mode = this.spec.stdio.stderr;
|
|
299
|
-
session.onErrData = (data) => {
|
|
300
|
-
if (mode === "pipe") this.stderr?.write(data);
|
|
301
|
-
else if (mode === "inherit") process.stderr.write(data);
|
|
302
|
-
else this.stderrCollector?.push(data);
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
};
|
|
306
|
-
//#endregion
|
|
307
|
-
//#region src/remote/remote-terminal.ts
|
|
308
|
-
/**
|
|
309
|
-
* SSH PTY allocation and process-session ownership for the subprocess seam.
|
|
310
|
-
* Ported from UynajGI/dsh-ssh (MIT) — the raw ssh2 shell channel is replaced
|
|
311
|
-
* by the engine's openShell session.
|
|
312
|
-
*/
|
|
313
|
-
/** Resolve after one duration. */
|
|
314
|
-
function delay(ms) {
|
|
315
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
316
|
-
}
|
|
317
|
-
/** One SSH PTY and its remote login shell, projected onto the subprocess terminal seam. */
|
|
318
|
-
var SshTerminalHandle = class {
|
|
319
|
-
closeChannel;
|
|
320
|
-
sendChannel;
|
|
321
|
-
signalChannel;
|
|
322
|
-
graceMs;
|
|
323
|
-
pid = -1;
|
|
324
|
-
output = new PassThrough();
|
|
325
|
-
done;
|
|
326
|
-
exitResolve;
|
|
327
|
-
topLevelExited = false;
|
|
328
|
-
cleanup;
|
|
329
|
-
constructor(closeChannel, sendChannel, signalChannel, graceMs) {
|
|
330
|
-
this.closeChannel = closeChannel;
|
|
331
|
-
this.sendChannel = sendChannel;
|
|
332
|
-
this.signalChannel = signalChannel;
|
|
333
|
-
this.graceMs = graceMs;
|
|
334
|
-
this.done = new Promise((resolve) => {
|
|
335
|
-
this.exitResolve = resolve;
|
|
336
|
-
});
|
|
337
|
-
}
|
|
338
|
-
/** Settle the exit promise (called by the runtime when the channel closes). */
|
|
339
|
-
resolveExit(outcome) {
|
|
340
|
-
this.exitResolve(outcome);
|
|
341
|
-
}
|
|
342
|
-
/** @inheritdoc */
|
|
343
|
-
write(data) {
|
|
344
|
-
if (this.topLevelExited) return Promise.reject(/* @__PURE__ */ new Error("terminal process has exited"));
|
|
345
|
-
return new Promise((resolve, reject) => {
|
|
346
|
-
try {
|
|
347
|
-
this.sendChannel(data);
|
|
348
|
-
resolve();
|
|
349
|
-
} catch (error) {
|
|
350
|
-
reject(error instanceof Error ? error : new Error(String(error)));
|
|
351
|
-
}
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
/** The SSH channel does not expose a foreground process group. */
|
|
355
|
-
inspectForeground() {
|
|
356
|
-
return Promise.resolve(void 0);
|
|
357
|
-
}
|
|
358
|
-
/** @inheritdoc */
|
|
359
|
-
signalForeground(_signal) {
|
|
360
|
-
return Promise.reject(/* @__PURE__ */ new Error("subprocess-ssh: cannot resolve the foreground process group over an SSH channel"));
|
|
361
|
-
}
|
|
362
|
-
/** @inheritdoc */
|
|
363
|
-
terminate() {
|
|
364
|
-
if (this.cleanup !== void 0) return this.cleanup;
|
|
365
|
-
const cleanup = this.closeOnce();
|
|
366
|
-
this.cleanup = cleanup;
|
|
367
|
-
cleanup.catch(() => {
|
|
368
|
-
this.cleanup = void 0;
|
|
369
|
-
});
|
|
370
|
-
return cleanup;
|
|
371
|
-
}
|
|
372
|
-
signal(name) {
|
|
373
|
-
try {
|
|
374
|
-
this.signalChannel(name);
|
|
375
|
-
} catch {}
|
|
376
|
-
}
|
|
377
|
-
async closeOnce() {
|
|
378
|
-
this.signal("TERM");
|
|
379
|
-
await Promise.race([this.done.then(() => void 0, () => void 0), delay(this.graceMs)]);
|
|
380
|
-
if (!this.topLevelExited) this.signal("KILL");
|
|
381
|
-
await Promise.race([this.done.then(() => void 0, () => void 0), delay(this.graceMs)]);
|
|
382
|
-
if (!this.topLevelExited) {
|
|
383
|
-
this.closeChannel();
|
|
384
|
-
throw new Error("subprocess-ssh: terminal cleanup failed; channel still open");
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
};
|
|
388
|
-
/**
|
|
389
|
-
* Allocate an SSH PTY, replace its login shell with the requested argv, and
|
|
390
|
-
* return the live terminal handle.
|
|
391
|
-
*/
|
|
392
|
-
async function spawnSshTerminal(engine, getState, spec) {
|
|
393
|
-
spec.signal?.throwIfAborted();
|
|
394
|
-
const program = spec.argv[0];
|
|
395
|
-
if (program === void 0 || program.length === 0) throw new Error("subprocess-ssh: terminal argv must contain a program");
|
|
396
|
-
const state = getState();
|
|
397
|
-
if (state.mode !== "remote" || state.alias === void 0) throw new Error("subprocess-ssh: not in remote mode — switch the GUI to SSH mode first");
|
|
398
|
-
const environment = serializeEnvironment(await readScrubbedRemoteEnvironment(engine, state.alias), spec.env);
|
|
399
|
-
const session = await engine.openShell(state.alias, {
|
|
400
|
-
cols: spec.cols,
|
|
401
|
-
rows: spec.rows
|
|
402
|
-
});
|
|
403
|
-
const root = state.remoteRoot;
|
|
404
|
-
if (root === void 0) throw new Error("subprocess-ssh: remote workspace root is not set");
|
|
405
|
-
const cwd = spec.cwd !== void 0 && spec.cwd.startsWith("/") ? spec.cwd : root;
|
|
406
|
-
const handle = new SshTerminalHandle(() => session.close(), (data) => session.send(data), (name) => session.signal(name), spec.graceMs);
|
|
407
|
-
session.onData = (data) => {
|
|
408
|
-
if (!handle.output.destroyed) handle.output.write(data);
|
|
409
|
-
};
|
|
410
|
-
session.onExit = (code, error) => {
|
|
411
|
-
handle.topLevelExited = true;
|
|
412
|
-
handle.output.end();
|
|
413
|
-
handle.resolveExit({
|
|
414
|
-
exitCode: error !== void 0 ? null : code,
|
|
415
|
-
signal: null
|
|
416
|
-
});
|
|
417
|
-
};
|
|
418
|
-
const argv = spec.argv.map(shellQuote).join(" ");
|
|
419
|
-
await handle.write(`cd ${shellQuote(cwd)} && exec env -i -- ${environment} ${argv}\r`);
|
|
420
|
-
spec.signal?.throwIfAborted();
|
|
421
|
-
return handle;
|
|
422
|
-
}
|
|
423
|
-
//#endregion
|
|
424
|
-
//#region src/remote/remote-subprocess.ts
|
|
425
|
-
/**
|
|
426
|
-
* Remote subprocess provider for the `ctx.subprocess` capability seam: each
|
|
427
|
-
* spawn opens a streaming exec channel (or a PTY for terminals) on the
|
|
428
|
-
* current SSH-mode host through the dsh-ssh engine; output spill files stay
|
|
429
|
-
* on the local host. Ported from UynajGI/dsh-ssh (MIT).
|
|
430
|
-
*
|
|
431
|
-
* @module dsh-hardssh/remote-subprocess
|
|
432
|
-
*/
|
|
433
|
-
/**
|
|
434
|
-
* Enforce the seam's documented grace bound (positive, finite, one Node timer).
|
|
435
|
-
*/
|
|
436
|
-
function requireRepresentableGrace(graceMs) {
|
|
437
|
-
if (!Number.isFinite(graceMs) || graceMs <= 0 || graceMs > MAX_TIMER_DELAY_MS) throw new Error(`subprocess graceMs must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`);
|
|
438
|
-
}
|
|
439
|
-
/** SSH command manager registered as `ctx.subprocess` (remote mode). */
|
|
440
|
-
var SshSubprocessRuntime = class extends SubprocessRuntime {
|
|
441
|
-
engine;
|
|
442
|
-
getState;
|
|
443
|
-
live = /* @__PURE__ */ new Set();
|
|
444
|
-
terminals = /* @__PURE__ */ new Set();
|
|
445
|
-
spillDir = mkdtempSync(join(tmpdir(), "dsh-subprocess-ssh-"));
|
|
446
|
-
disposing = false;
|
|
447
|
-
constructor(ctx, engine, getState) {
|
|
448
|
-
super(ctx);
|
|
449
|
-
this.engine = engine;
|
|
450
|
-
this.getState = getState;
|
|
451
|
-
ctx.effect(() => async () => {
|
|
452
|
-
this.disposing = true;
|
|
453
|
-
const handles = [...this.live];
|
|
454
|
-
const terminals = [...this.terminals];
|
|
455
|
-
const pending = [];
|
|
456
|
-
for (const handle of handles) {
|
|
457
|
-
handle.terminate();
|
|
458
|
-
pending.push(handle.waitForExit().then(() => {
|
|
459
|
-
this.live.delete(handle);
|
|
460
|
-
}));
|
|
461
|
-
}
|
|
462
|
-
for (const terminal of terminals) pending.push(terminal.terminate().then(() => {
|
|
463
|
-
this.terminals.delete(terminal);
|
|
464
|
-
}));
|
|
465
|
-
const failures = (await Promise.allSettled(pending)).flatMap((outcome) => outcome.status === "rejected" ? [outcome.reason] : []);
|
|
466
|
-
if (failures.length === 1) throw failures[0];
|
|
467
|
-
if (failures.length > 1) throw new AggregateError(failures, "subprocess-ssh: teardown failed");
|
|
468
|
-
}, "ssh subprocess teardown");
|
|
469
|
-
}
|
|
470
|
-
/** @inheritdoc */
|
|
471
|
-
async resolveExecutable(command, env, signal) {
|
|
472
|
-
if (command.length === 0) throw new Error("subprocess-ssh: executable name must be non-empty");
|
|
473
|
-
signal?.throwIfAborted();
|
|
474
|
-
const state = this.getState();
|
|
475
|
-
if (state.mode !== "remote" || state.alias === void 0) throw new Error("subprocess-ssh: not in remote mode — switch the GUI to SSH mode first");
|
|
476
|
-
if (posix.isAbsolute(command)) {
|
|
477
|
-
const result = await this.engine.exec(state.alias, `test -f ${shellQuote(command)} -a -x ${shellQuote(command)}`, 1e4);
|
|
478
|
-
signal?.throwIfAborted();
|
|
479
|
-
if (!result.success || result.exitCode !== 0) throw new Error(`subprocess-ssh: command ${JSON.stringify(command)} is not an executable file`);
|
|
480
|
-
return command;
|
|
481
|
-
}
|
|
482
|
-
if (command.includes("/")) throw new Error(`subprocess-ssh: command ${JSON.stringify(command)} is a relative path; use an absolute path or a bare PATH name`);
|
|
483
|
-
const path = env?.PATH;
|
|
484
|
-
const prefix = path === void 0 ? "" : `PATH=${shellQuote(path)} `;
|
|
485
|
-
const result = await this.engine.exec(state.alias, `${prefix}command -v -- ${shellQuote(command)}`, 1e4);
|
|
486
|
-
signal?.throwIfAborted();
|
|
487
|
-
const executable = result.stdout.trim();
|
|
488
|
-
if (!result.success || result.exitCode !== 0 || executable.length === 0 || executable.includes("\n") || !posix.isAbsolute(executable) && !executable.includes("/")) throw new Error(`subprocess-ssh: executable ${JSON.stringify(command)} did not resolve to one absolute path`);
|
|
489
|
-
const root = state.remoteRoot;
|
|
490
|
-
if (root === void 0) throw new Error("subprocess-ssh: remote workspace root is not set");
|
|
491
|
-
return posix.isAbsolute(executable) ? executable : posix.resolve(root, executable);
|
|
492
|
-
}
|
|
493
|
-
/** @inheritdoc */
|
|
494
|
-
spawn(spec) {
|
|
495
|
-
if (this.disposing) throw new Error("subprocess-ssh: service is disposing");
|
|
496
|
-
const program = spec.argv[0];
|
|
497
|
-
if (program === void 0 || program.length === 0) throw new Error("invalid argv: expected a non-empty program name at argv[0]");
|
|
498
|
-
requireRepresentableGrace(spec.graceMs);
|
|
499
|
-
if (spec.signal?.aborted === true) throw new Error(`aborted before spawn: ${String(spec.signal.reason)}`);
|
|
500
|
-
const handle = new SshSubprocessHandle(this.engine, this.getState, spec, this.spillDir);
|
|
501
|
-
this.live.add(handle);
|
|
502
|
-
const release = async () => {
|
|
503
|
-
await handle.waitForExit();
|
|
504
|
-
this.live.delete(handle);
|
|
505
|
-
};
|
|
506
|
-
handle.done.then(release, release).catch(() => {});
|
|
507
|
-
return handle;
|
|
508
|
-
}
|
|
509
|
-
/** @inheritdoc */
|
|
510
|
-
async spawnTerminal(spec) {
|
|
511
|
-
if (this.disposing) throw new Error("subprocess-ssh: service is disposing");
|
|
512
|
-
const program = spec.argv[0];
|
|
513
|
-
if (program === void 0 || program.length === 0) throw new Error("subprocess-ssh: terminal argv must contain a program");
|
|
514
|
-
requireRepresentableGrace(spec.graceMs);
|
|
515
|
-
spec.signal?.throwIfAborted();
|
|
516
|
-
const terminal = await spawnSshTerminal(this.engine, this.getState, spec);
|
|
517
|
-
if (this.disposing) {
|
|
518
|
-
await terminal.terminate();
|
|
519
|
-
throw new Error("subprocess-ssh: service disposed during terminal setup");
|
|
520
|
-
}
|
|
521
|
-
this.terminals.add(terminal);
|
|
522
|
-
const release = async () => {
|
|
523
|
-
await terminal.terminate();
|
|
524
|
-
this.terminals.delete(terminal);
|
|
525
|
-
};
|
|
526
|
-
terminal.done.then(release, release).catch(() => {});
|
|
527
|
-
return terminal;
|
|
528
|
-
}
|
|
529
|
-
};
|
|
530
|
-
//#endregion
|
|
531
5
|
//#region src/switch/switch-subprocess.ts
|
|
532
6
|
/**
|
|
533
7
|
* The `ctx.subprocess` switching facade: routes every spawn to the execution
|
|
@@ -539,6 +13,36 @@ var SshSubprocessRuntime = class extends SubprocessRuntime {
|
|
|
539
13
|
*
|
|
540
14
|
* @module dsh-hardssh/switch-subprocess
|
|
541
15
|
*/
|
|
16
|
+
const DEFAULT_CLIENT_TOOL_NAMES = [
|
|
17
|
+
"pwsh",
|
|
18
|
+
"powershell",
|
|
19
|
+
"cmd"
|
|
20
|
+
];
|
|
21
|
+
const CLIENT_EXECUTABLE_RE = /\.(exe|cmd|bat|ps1|com)$/i;
|
|
22
|
+
/**
|
|
23
|
+
* Client-packaged helpers that search the WORKSPACE's files (the bundled
|
|
24
|
+
* ripgrep behind the glob/grep tools). They are client binaries, but they are
|
|
25
|
+
* not shells: running one locally while the session's world is remote searches
|
|
26
|
+
* the local anchor directory (an empty placeholder) and reports "no matches",
|
|
27
|
+
* and sending the client's absolute PATH to the server cannot work either.
|
|
28
|
+
* Either way the answer is wrong, so a path-shaped invocation is refused
|
|
29
|
+
* loudly. A BARE `rg` is left alone: that is an ordinary server command and
|
|
30
|
+
* belongs to the remote world like any other.
|
|
31
|
+
*/
|
|
32
|
+
const CLIENT_WORKSPACE_SEARCH_RE = /^(rg|ripgrep)(\.exe)?$/i;
|
|
33
|
+
/** True for a PATH-shaped (not bare-name) invocation of a search helper. */
|
|
34
|
+
function isClientSearchHelperPath(exe) {
|
|
35
|
+
if (!exe.includes("/") && !exe.includes("\\")) return false;
|
|
36
|
+
const base = exe.split(/[\\/]/).pop() ?? exe;
|
|
37
|
+
return CLIENT_WORKSPACE_SEARCH_RE.test(base);
|
|
38
|
+
}
|
|
39
|
+
/** A client-native executable: Windows-format path/extension, or a bare name
|
|
40
|
+
* on the declared client-tool list (remote POSIX hosts never carry these). */
|
|
41
|
+
function isClientNativeExecutable(exe, names) {
|
|
42
|
+
if (exe === "") return false;
|
|
43
|
+
if (/^[a-zA-Z]:[\\/]/.test(exe) || exe.includes("\\") || CLIENT_EXECUTABLE_RE.test(exe)) return true;
|
|
44
|
+
return names.includes(exe);
|
|
45
|
+
}
|
|
542
46
|
/** Workspace-routing subprocess facade. */
|
|
543
47
|
var SwitchSubprocessRuntime = class extends SubprocessRuntime {
|
|
544
48
|
deps;
|
|
@@ -546,55 +50,94 @@ var SwitchSubprocessRuntime = class extends SubprocessRuntime {
|
|
|
546
50
|
super(ctx);
|
|
547
51
|
this.deps = deps;
|
|
548
52
|
}
|
|
549
|
-
/** The runtime for one spec (by its cwd). */
|
|
53
|
+
/** The runtime for one spec (by its cwd); undefined means LOCAL. */
|
|
550
54
|
runtimeFor(cwd) {
|
|
551
55
|
return this.deps.worldFor(cwd);
|
|
552
56
|
}
|
|
57
|
+
/** Client binaries run on THIS machine even from a bound workspace — their
|
|
58
|
+
* executables cannot exist on the remote POSIX host, and the spawn cwd is
|
|
59
|
+
* the local anchor (which exists locally), so local execution is sound.
|
|
60
|
+
* Everything else runs in the session's world (remote on a bound host).
|
|
61
|
+
*
|
|
62
|
+
* ONE exception is refused rather than routed: a client-side SEARCH helper
|
|
63
|
+
* (rg) in a remote-bound session would search the empty local anchor and
|
|
64
|
+
* report "no matches" — a confidently wrong answer about the server's
|
|
65
|
+
* contents. There is no way to make a local index-of-files tool read the
|
|
66
|
+
* remote workspace, so the caller is told to use the remote tools.
|
|
67
|
+
*
|
|
68
|
+
* Locality comes from the routing answer (`undefined`), never from an
|
|
69
|
+
* identity comparison against `deps.local`: that comparison silently failed
|
|
70
|
+
* once and refused local ripgrep launches for every session. */
|
|
71
|
+
effectiveRuntime(spec) {
|
|
72
|
+
const runtime = this.runtimeFor(spec.cwd);
|
|
73
|
+
if (runtime === void 0) return this.deps.local;
|
|
74
|
+
const names = this.deps.clientToolNames ?? DEFAULT_CLIENT_TOOL_NAMES;
|
|
75
|
+
const exe = spec.argv !== void 0 && spec.argv.length > 0 ? spec.argv[0] : "";
|
|
76
|
+
if (isClientSearchHelperPath(exe)) throw new Error(`dsh-hardssh: '${exe.split(/[\\/]/).pop()}' is a client-side search tool and cannot read the remote workspace bound to this session (its working directory is the local anchor placeholder, not the server). Search remote content with the remote_search tool (mode="glob" for file names, mode="grep" for content) or ssh_exec instead.`);
|
|
77
|
+
return isClientNativeExecutable(exe, names) ? this.deps.local : runtime;
|
|
78
|
+
}
|
|
553
79
|
/** @inheritdoc */
|
|
554
80
|
resolveExecutable(command, env, signal) {
|
|
555
81
|
return this.deps.local.resolveExecutable(command, env, signal);
|
|
556
82
|
}
|
|
557
83
|
/** @inheritdoc */
|
|
558
84
|
spawn(spec) {
|
|
559
|
-
return this.
|
|
85
|
+
return this.effectiveRuntime(spec).spawn(spec);
|
|
560
86
|
}
|
|
561
87
|
/** @inheritdoc */
|
|
562
88
|
spawnTerminal(spec) {
|
|
563
|
-
return this.
|
|
89
|
+
return this.effectiveRuntime(spec).spawnTerminal(spec);
|
|
564
90
|
}
|
|
565
91
|
};
|
|
566
92
|
//#endregion
|
|
567
93
|
//#region src/subprocess.ts
|
|
568
94
|
/** Stable cordis plugin name. */
|
|
569
95
|
const name = "hardssh-subprocess";
|
|
570
|
-
/**
|
|
571
|
-
const inject = ["
|
|
572
|
-
/**
|
|
573
|
-
function
|
|
574
|
-
return () =>
|
|
575
|
-
mode: "remote",
|
|
576
|
-
alias,
|
|
577
|
-
remoteRoot
|
|
578
|
-
});
|
|
96
|
+
/** Workspace routing is exclusively provided by the generic core. */
|
|
97
|
+
const inject = ["workspaceCore"];
|
|
98
|
+
/** The record behind a router connection (sync snapshot lookup). */
|
|
99
|
+
function genericRecordFor(core, id) {
|
|
100
|
+
return core.ledger.snapshotSync().records.find((record) => record.id === id);
|
|
579
101
|
}
|
|
580
|
-
/**
|
|
102
|
+
/** Resolve the subprocess runtime owning a cwd, or undefined when local. */
|
|
103
|
+
function genericSubprocessFor(core, cwd, reservedAnchorRoots) {
|
|
104
|
+
if (!core.isReady()) {
|
|
105
|
+
if (cwd !== void 0 && reservedAnchorRoots.some((root) => isPathUnderAnchor(root, cwd))) throw new Error("subprocess-ssh: workspace routing is unavailable while the generic workspace core is not ready (anchor path fails closed)");
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const connection = core.router.fromAnchor(cwd);
|
|
109
|
+
if (connection === void 0) return void 0;
|
|
110
|
+
const record = genericRecordFor(core, connection.workspaceId);
|
|
111
|
+
if (record === void 0) throw new Error(`subprocess-workspace: routed workspace '${connection.workspaceId}' is missing from the ledger`);
|
|
112
|
+
const runtime = connection.get("workspace.process");
|
|
113
|
+
if (runtime === void 0) throw new Error(`subprocess-workspace: workspace '${record.id}' provides no workspace.process capability`);
|
|
114
|
+
return runtime;
|
|
115
|
+
}
|
|
116
|
+
/** Mount the generic switching subprocess facade. */
|
|
581
117
|
function apply(ctx) {
|
|
582
|
-
const core = ctx.hardsshCore;
|
|
583
118
|
const localCtx = ctx.isolate("subprocess");
|
|
584
119
|
const localSubprocess = new LocalSubprocessRuntime(localCtx);
|
|
585
|
-
|
|
586
|
-
const
|
|
120
|
+
const ws = ctx.workspaceCore;
|
|
121
|
+
const anchorRootDir = anchorRoot();
|
|
587
122
|
let warnedUnready = false;
|
|
588
123
|
new SwitchSubprocessRuntime(ctx, {
|
|
589
124
|
local: localSubprocess,
|
|
590
125
|
worldFor: (cwd) => {
|
|
591
|
-
if (!
|
|
592
|
-
|
|
593
|
-
|
|
126
|
+
if (!ws.isReady()) {
|
|
127
|
+
if (cwd !== void 0 && isPathUnderAnchor(anchorRootDir, cwd)) {
|
|
128
|
+
if (!warnedUnready) {
|
|
129
|
+
warnedUnready = true;
|
|
130
|
+
console.warn("[dsh-hardssh] subprocess routing is not ready yet (workspace core still initializing or failed) — refusing to run beneath the workspace anchor root");
|
|
131
|
+
}
|
|
132
|
+
throw new Error("subprocess-ssh: workspace routing is unavailable while the generic workspace core is not ready (anchor path fails closed)");
|
|
133
|
+
}
|
|
134
|
+
return;
|
|
594
135
|
}
|
|
595
|
-
|
|
136
|
+
const runtime = genericSubprocessFor(ws, cwd, [anchorRootDir]);
|
|
137
|
+
if (runtime !== void 0) return runtime;
|
|
138
|
+
if (cwd !== void 0 && isPathUnderAnchor(anchorRootDir, cwd)) throw new Error(`subprocess-ssh: '${cwd}' is inside the workspace anchor root but no registered workspace owns it (fail closed)`);
|
|
596
139
|
}
|
|
597
140
|
});
|
|
598
141
|
}
|
|
599
142
|
//#endregion
|
|
600
|
-
export { apply, inject, name };
|
|
143
|
+
export { apply, genericSubprocessFor, inject, name };
|