dsh-win-multi-bash 0.1.0

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.
@@ -0,0 +1,335 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import z from "@deepseek-ai/schemastery";
3
+ import { LocalBashExecutor } from "@deepseek-ai/dsh-bash-local";
4
+ import { classifyDenial, classifyRunnerFailure, matchesSignature } from "../vendor/helpers.js";
5
+ import { BWRAP_RUNNER_FAILURE_RULES, bwrapProfileArgs } from "../vendor/bwrap-profiles.js";
6
+ import { lstatSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ /**
9
+ * The bwrap denial dialect: a denied file effect prints this signature on
10
+ * stderr (mirrors DENIAL_SIGNATURES.bwrap in the base sandbox-local), so the
11
+ * sandbox facts the tool renders can report `denied` instead of always false.
12
+ */
13
+ const BWRAP_DENIAL_SIGNATURES = ["read-only file system"];
14
+ /**
15
+ * Derive a config schema from a base object schema WITHOUT mutating it.
16
+ * schemastery's Schema#set() mutates the shared `dict` in place, so chaining
17
+ * .set() on LocalBashExecutor.Config lets whichever subclass module loads
18
+ * last overwrite the earlier one's fields — git-bash's probeTimeoutMs default
19
+ * silently became wsl-bash's 30000 and its sandbox union gained the invalid
20
+ * 'bwrap' value. Spreading the base dict into a fresh z.object makes every
21
+ * derivation independent of both the base and other subclasses.
22
+ */
23
+ function deriveConfig(base, fields) {
24
+ const derived = z.object({ ...base.dict });
25
+ if (base.meta !== void 0) derived.meta = { ...base.meta };
26
+ for (const key of Object.keys(fields)) derived.set(key, fields[key]);
27
+ return derived;
28
+ }
29
+ //#region lib/types/resolve.js
30
+ /**
31
+ * WSL executable and distro resolution for the bash-wsl executor, dependency-free
32
+ * and parameterized (env/platform) so resolution is a pure function of its
33
+ * inputs on every platform — the twin of dsh-bash-git's resolveBashPath and
34
+ * dsh-pwsh-local's resolvePwshPath.
35
+ * @module @deepseek-ai/dsh-bash-wsl/resolve
36
+ */
37
+ /**
38
+ * Well-known WSL executable locations: System32 (the shipped wsl.exe) plus
39
+ * PATH entries.
40
+ * @param env - the environment to probe; defaults to the process environment.
41
+ * @returns candidate `wsl.exe` paths in resolution order.
42
+ */
43
+ function candidateWslPaths(env = process.env) {
44
+ const candidates = [join(env.SystemRoot ?? "C:\\Windows", "System32", "wsl.exe")];
45
+ for (const entry of (env.PATH ?? "").split(";")) {
46
+ const trimmed = entry.trim().replace(/^"|"$/g, "");
47
+ if (trimmed.length === 0) continue;
48
+ candidates.push(join(trimmed, "wsl.exe"));
49
+ }
50
+ return candidates;
51
+ }
52
+ function candidateExists(candidate) {
53
+ try {
54
+ const stat = lstatSync(candidate);
55
+ return stat.isFile() || stat.isSymbolicLink();
56
+ } catch {
57
+ return false;
58
+ }
59
+ }
60
+ /**
61
+ * Resolve the wsl.exe executable this executor spawns. Returns undefined on
62
+ * win32 when no candidate exists (the executor turns that into a loud
63
+ * route-time error naming the probes); non-win32 hosts resolve a bare
64
+ * `wsl` for PATH resolution, mirroring resolvePwshPath.
65
+ * @param configured - an explicit `wslPath` config value, trusted as-is.
66
+ * @param env - the environment to probe on Windows; defaults to the process environment.
67
+ * @param platform - the platform to resolve for; defaults to the process platform.
68
+ * @returns the first existing well-known location on win32, else `wsl`, else undefined.
69
+ */
70
+ function resolveWslPath(configured, env = process.env, platform = process.platform) {
71
+ if (configured !== void 0 && configured.length > 0) return configured;
72
+ if (platform === "win32") {
73
+ for (const candidate of candidateWslPaths(env)) if (candidateExists(candidate)) return candidate;
74
+ return;
75
+ }
76
+ return "wsl";
77
+ }
78
+ /**
79
+ * Parse `wsl -l -q` output into distro names. wsl.exe writes UTF-16LE with
80
+ * null-byte interleaving when stdout is redirected, so NUL bytes are stripped
81
+ * before splitting; surrounding quotes and blank lines are dropped.
82
+ * @param output - raw `-l -q` stdout.
83
+ * @returns distro names in output order.
84
+ */
85
+ function parseWslDistroList(output) {
86
+ const distros = [];
87
+ for (const raw of output.replace(/\0/g, "").split(/\r?\n/)) {
88
+ const line = raw.trim().replace(/^"|"$/g, "");
89
+ if (line.length === 0) continue;
90
+ distros.push(line);
91
+ }
92
+ return distros;
93
+ }
94
+ /**
95
+ * Convert a Windows drive-letter path to the WSL /mnt/<drive>/... form used
96
+ * as the bwrap workspace root inside the distro. Anything without a
97
+ * drive-letter form fails loud: wsl.exe's own `--cd` translation covers
98
+ * workdirs, but the bwrap profile needs an explicit Linux-side root.
99
+ * @param winPath - the Windows path to convert.
100
+ * @returns the Linux-side path.
101
+ * @throws Error when the path has no drive-letter form.
102
+ */
103
+ function toWslPath(winPath) {
104
+ const match = /^([A-Za-z]):[\\/](.+)$/.exec(winPath);
105
+ if (match === null) throw new Error(`unsupported Windows path: ${winPath}`);
106
+ const drive = match[1];
107
+ const rest = match[2];
108
+ /* v8 ignore next -- the regex guarantees both captures whenever it matches */
109
+ if (drive === void 0 || rest === void 0) throw new Error(`unsupported Windows path: ${winPath}`);
110
+ return `/mnt/${drive.toLowerCase()}/${rest.replaceAll("\\", "/")}`;
111
+ }
112
+ //#endregion
113
+ //#region lib/types/index.js
114
+ /**
115
+ * WSL Service Provider for the bash capability seam: a LocalBashExecutor
116
+ * whose argv is `[wslPath, --cd workdir?, -d distro?, --, bash, -c, payload]`.
117
+ * The command rides as a base64 payload through `echo <b64> | base64 -d | bash`
118
+ * so wsl.exe's Windows→Linux argument serialization can never damage quotes.
119
+ * Sandbox: `auto` probes bubblewrap inside the distro (once, lazily, at the
120
+ * first sandboxMode read or the first command) and runs unconfined without
121
+ * sandbox facts when absent; `bwrap` fails loud at first use when the probe
122
+ * fails; `none` never probes or confines.
123
+ * @module @deepseek-ai/dsh-bash-wsl
124
+ */
125
+ /**
126
+ * WSL executor over the local bash mechanics: argv is the resolved wsl.exe
127
+ * followed by `--cd`/`-d`/`--` `bash -c` with the base64 payload, and the
128
+ * sandbox stance (`auto`/`bwrap`/`none`) is applied lazily per the module
129
+ * doc's probe contract.
130
+ */
131
+ var WslBashExecutor = class extends LocalBashExecutor {
132
+ static Config = deriveConfig(LocalBashExecutor.Config, {
133
+ wslPath: z.string(),
134
+ wslDistro: z.string(),
135
+ sandbox: z.union([
136
+ z.const("auto"),
137
+ z.const("none"),
138
+ z.const("bwrap")
139
+ ]).default("auto"),
140
+ probeTimeoutMs: z.natural().default(3e4),
141
+ requireSandbox: z.boolean().default(false)
142
+ });
143
+ /** Test hook mirroring the sandbox-local internals pattern. */
144
+ internals = {};
145
+ sandboxStance;
146
+ probeTimeoutMs;
147
+ bwrapVerdict;
148
+ distroProbed = false;
149
+ distroVerdict;
150
+ constructor(ctx, config) {
151
+ super(ctx, config);
152
+ const entry = config;
153
+ this.sandboxStance = entry.sandbox;
154
+ this.probeTimeoutMs = entry.probeTimeoutMs;
155
+ this.requireSandbox = entry.requireSandbox;
156
+ }
157
+ /**
158
+ * The capability fact: the policy default mode while bwrap is usable. With
159
+ * `requireSandbox`, the mode is declared even when the probe fails, so the
160
+ * tool layer advertises escalation (`sandbox_permissions`) and the executor
161
+ * can refuse unconfined runs outside danger-full-access.
162
+ */
163
+ get sandboxMode() {
164
+ if (this.sandboxStance === "none") return void 0;
165
+ if (this.requireSandbox) return this.ctx.sandboxPolicy.defaultMode;
166
+ return this.requireBwrapUsable() ? this.ctx.sandboxPolicy.defaultMode : void 0;
167
+ }
168
+ /** Auto: false means run unconfined. Explicit bwrap: a failed probe throws loud. */
169
+ requireBwrapUsable() {
170
+ if (this.probeBwrapOnce()) return true;
171
+ if (this.sandboxStance === "bwrap") throw new Error("bash-wsl: bwrap was not found in the WSL distro (explicit sandbox \"bwrap\" is configured); install bubblewrap or set sandbox: auto/none");
172
+ return false;
173
+ }
174
+ tryWslPath() {
175
+ return (this.internals.resolveWslPath ?? ((configured) => resolveWslPath(configured)))(this.config.wslPath);
176
+ }
177
+ wslPath() {
178
+ const path = this.tryWslPath();
179
+ if (path !== void 0) return path;
180
+ throw new Error(`bash-wsl: WSL was not found (probed ${candidateWslPaths().join(", ")}). Enable Windows Subsystem for Linux or set wslBash.wslPath.`);
181
+ }
182
+ probeDistroOnce() {
183
+ if (!this.distroProbed) {
184
+ this.distroProbed = true;
185
+ const probe = this.internals.probeDistro;
186
+ if (probe !== void 0) return this.distroVerdict = probe();
187
+ const wsl = this.tryWslPath();
188
+ if (wsl === void 0) {
189
+ this.distroVerdict = void 0;
190
+ /* v8 ignore next 2 -- every executor path resolves wslPath before this probe, so the guard is defensive only. */
191
+ return;
192
+ }
193
+ const run = spawnSync(wsl, ["-l", "-q"], {
194
+ timeout: this.probeTimeoutMs,
195
+ encoding: "utf8"
196
+ });
197
+ this.distroVerdict = run.status === 0 ? parseWslDistroList(run.stdout)[0] : void 0;
198
+ }
199
+ return this.distroVerdict;
200
+ }
201
+ distro() {
202
+ return this.config.wslDistro ?? this.probeDistroOnce();
203
+ }
204
+ probeBwrapOnce() {
205
+ if (this.bwrapVerdict !== void 0) return this.bwrapVerdict;
206
+ const probe = this.internals.probeBwrap;
207
+ if (probe !== void 0) return this.bwrapVerdict = probe();
208
+ const wsl = this.tryWslPath();
209
+ if (wsl === void 0) return this.bwrapVerdict = false;
210
+ const distro = this.distro();
211
+ const run = spawnSync(wsl, [
212
+ ...distro !== void 0 ? ["-d", distro] : [],
213
+ "-e",
214
+ "bash",
215
+ "-c",
216
+ "command -v bwrap"
217
+ ], {
218
+ timeout: this.probeTimeoutMs,
219
+ stdio: "ignore"
220
+ });
221
+ return this.bwrapVerdict = run.status === 0;
222
+ }
223
+ payload(command) {
224
+ return `echo ${Buffer.from(command, "utf8").toString("base64")} | base64 -d | bash`;
225
+ }
226
+ /** The workdir `resolve()` fills when a request omits one. */
227
+ defaultWorkdir() {
228
+ return this.config.cwd ?? process.cwd();
229
+ }
230
+ /**
231
+ * The plain WSL argv for one spec (no sandbox). `--cd` is passed only when
232
+ * the workdir differs from the default, so the subprocess cwd and the WSL
233
+ * start directory agree without a redundant wsl.exe translation; a Linux
234
+ * workdir passes through verbatim.
235
+ */
236
+ argv(spec) {
237
+ const head = [this.wslPath()];
238
+ if (spec.workdir !== this.defaultWorkdir()) head.push("--cd", spec.workdir);
239
+ const distro = this.distro();
240
+ if (distro !== void 0) head.push("-d", distro);
241
+ head.push("--", "bash", "-c", this.payload(spec.command));
242
+ return head;
243
+ }
244
+ confinedPolicy(spec) {
245
+ return spec.sandboxPolicy ?? this.ctx.sandboxPolicy.resolve();
246
+ }
247
+ /** The bwrap-wrapped argv; the workspace root is translated to its /mnt/<drive> form. */
248
+ bwrapArgv(spec) {
249
+ const policy = this.confinedPolicy(spec);
250
+ const linuxRoot = toWslPath(policy.workspaceRoot);
251
+ const head = [this.wslPath()];
252
+ if (spec.workdir !== this.defaultWorkdir()) head.push("--cd", spec.workdir);
253
+ const distro = this.distro();
254
+ if (distro !== void 0) head.push("-d", distro);
255
+ const mode = policy.mode;
256
+ head.push("--", "bwrap", ...bwrapProfileArgs({
257
+ mode,
258
+ workspaceRoot: linuxRoot
259
+ }), "bash", "-c", this.payload(spec.command));
260
+ return head;
261
+ }
262
+ async run(spec) {
263
+ const policy = this.confinedPolicy(spec);
264
+ if (policy.mode === "danger-full-access") return {
265
+ ...await this.runArgv(spec, this.argv(spec)),
266
+ sandbox: {
267
+ mode: "danger-full-access",
268
+ denied: false
269
+ }
270
+ };
271
+ if (this.sandboxStance === "none") return this.runArgv(spec, this.argv(spec));
272
+ if (!this.requireBwrapUsable()) {
273
+ if (this.requireSandbox) throw new Error("bash-wsl: sandbox unavailable — bwrap was not found in the WSL distro; refusing to run unconfined under " + policy.mode + " mode. Install bubblewrap or retry the command with sandbox_permissions: \"danger-full-access\" and a justification.");
274
+ return this.runArgv(spec, this.argv(spec));
275
+ }
276
+ const result = await this.runArgv(spec, this.bwrapArgv(spec));
277
+ if (classifyRunnerFailure(result.exitCode, result.stderr.text, BWRAP_RUNNER_FAILURE_RULES) !== void 0) return {
278
+ ...result,
279
+ sandbox: {
280
+ mode: policy.mode,
281
+ denied: false,
282
+ enforcement: "full",
283
+ runnerFailed: true
284
+ }
285
+ };
286
+ return {
287
+ ...result,
288
+ sandbox: {
289
+ mode: policy.mode,
290
+ denied: classifyDenial(result, BWRAP_DENIAL_SIGNATURES),
291
+ enforcement: "full"
292
+ }
293
+ };
294
+ }
295
+ /** Per-process confinement facts retained until settlement (bash-sandbox pattern). */
296
+ processFacts = /* @__PURE__ */ new Map();
297
+ start(spec) {
298
+ const policy = this.confinedPolicy(spec);
299
+ if (policy.mode === "danger-full-access") return this.startArgv(spec, this.argv(spec));
300
+ if (this.sandboxStance === "none") return this.startArgv(spec, this.argv(spec));
301
+ if (!this.requireBwrapUsable()) {
302
+ if (this.requireSandbox) throw new Error("bash-wsl: sandbox unavailable — bwrap was not found in the WSL distro; refusing to run unconfined under " + policy.mode + " mode. Install bubblewrap or retry the command with sandbox_permissions: \"danger-full-access\" and a justification.");
303
+ return this.startArgv(spec, this.argv(spec));
304
+ }
305
+ const proc = this.startArgv(spec, this.bwrapArgv(spec));
306
+ this.processFacts.set(proc, { mode: policy.mode });
307
+ return proc;
308
+ }
309
+ /** Stamp per-process sandbox facts before `done` settles (bash-sandbox semantics). */
310
+ onProcessDone(proc, stderr, spawnFailed, spawnError) {
311
+ const facts = this.processFacts.get(proc);
312
+ if (facts !== void 0) {
313
+ this.processFacts.delete(proc);
314
+ proc.sandbox = classifyRunnerFailure(proc.exitCode, stderr, BWRAP_RUNNER_FAILURE_RULES) !== void 0 ? {
315
+ mode: facts.mode,
316
+ denied: false,
317
+ enforcement: "full",
318
+ runnerFailed: true
319
+ } : {
320
+ mode: facts.mode,
321
+ denied: matchesSignature(proc.exitCode, stderr, BWRAP_DENIAL_SIGNATURES),
322
+ enforcement: "full"
323
+ };
324
+ }
325
+ super.onProcessDone(proc, stderr, spawnFailed, spawnError);
326
+ }
327
+ resolve(request) {
328
+ return {
329
+ ...super.resolve(request),
330
+ sandboxPolicy: request.sandboxPolicy ?? this.ctx.sandboxPolicy.resolve()
331
+ };
332
+ }
333
+ };
334
+ //#endregion
335
+ export { WslBashExecutor, WslBashExecutor as default };
@@ -0,0 +1,23 @@
1
+ //#region lib/types/invariant.js
2
+ /**
3
+ * Package-owned invariant companion for `@deepseek-ai/dsh-bash-wsl`.
4
+ * @module @deepseek-ai/dsh-bash-wsl/invariant
5
+ */
6
+ const PACKAGE_NAME = "@deepseek-ai/dsh-bash-wsl";
7
+ /** Cordis companion plugin name. */
8
+ const name = "bash-wsl-invariant";
9
+ /** Service required before the companion can reserve package ownership. */
10
+ const inject = ["invariants"];
11
+ /**
12
+ * No runtime invariant: this package exposes no independent event sequence or mutable data relation
13
+ * beyond contracts enforced at its owning seam.
14
+ */
15
+ const install = () => {};
16
+ /**
17
+ * Register this package's invariant companion.
18
+ * @param ctx - Cordis context carrying the invariant service.
19
+ * @returns the installed registration's disposer after setup succeeds.
20
+ */
21
+ const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
22
+ //#endregion
23
+ export { apply, inject, name };
package/lib/index.js ADDED
@@ -0,0 +1,20 @@
1
+ // dsh-win-multi-bash — self-contained Windows multi-bash plugin.
2
+ //
3
+ // This package carries the whole feature implementation, bundled from the
4
+ // DeepSeek Harness project's shell packages (see THIRD_PARTY_NOTICES),
5
+ // compiled to plain ESM JS:
6
+ //
7
+ // lib/shell-select/ — ShellSelectExecutor (the ctx.shell selector)
8
+ // lib/bash-git/ — GitBashExecutor (MSYS / Git for Windows)
9
+ // lib/bash-wsl/ — WslBashExecutor (WSL distros, base64 payloads)
10
+ // lib/tool-bash/ — defineShellTool factory + git_bash / wsl_bash instances
11
+ // lib/vendor/ — helpers.js (bash-sandbox classification) and
12
+ // bwrap-profiles.js (bwrap rules/args) — the two
13
+ // helper modules not exported by the base runtime.
14
+ //
15
+ // Only published @deepseek-ai base packages are imported (dsh-shell,
16
+ // dsh-sandbox, dsh-pwsh-sandbox, dsh-tools, dsh-llm, ...), so the plugin runs
17
+ // on any standard deployment — no additional runtime packages required.
18
+ //
19
+ // The composition wiring lives in cordis.patch.yml.
20
+ export {}
@@ -0,0 +1,169 @@
1
+ import z from "@deepseek-ai/schemastery";
2
+ import { HarnessError } from "@deepseek-ai/dsh-llm";
3
+ import { SHELL_SETTINGS_NAMESPACE, ShellExecutor } from "@deepseek-ai/dsh-shell";
4
+ import { installSettingsSection } from "@deepseek-ai/dsh-settings";
5
+ import { GitBashExecutor } from "../bash-git/index.js";
6
+ import { WslBashExecutor } from "../bash-wsl/index.js";
7
+ import { SandboxPwshExecutor } from "@deepseek-ai/dsh-pwsh-sandbox";
8
+ import { assertServiceableBashConfig } from "@deepseek-ai/dsh-bash-local";
9
+ import { assertServiceablePwshConfig } from "@deepseek-ai/dsh-pwsh-local";
10
+ //#region lib/types/index.js
11
+ /**
12
+ * Selector Service Provider for the bash capability seam: occupies the single
13
+ * `ctx.shell` seat and routes each request to one of several backends held as
14
+ * plain instances. The tool name selects the backend (`request.shell`); a
15
+ * request without one goes to the configured `default`. Backends are
16
+ * constructed on their own child fibers with isolated `shell`/`settings`
17
+ * scopes so their inherited Service registration and settings wiring cannot
18
+ * collide with this executor's own seat. win32 compositions only; POSIX
19
+ * compositions keep a single backend directly on the seat.
20
+ * @module @deepseek-ai/dsh-shell-select
21
+ */
22
+ /** Error code for routing to an unknown or disabled backend. */
23
+ const SHELL_BACKEND_UNAVAILABLE = "SHELL_BACKEND_UNAVAILABLE";
24
+ const DEFAULT_FACTORIES = {
25
+ "git-bash": (ctx, config) => new GitBashExecutor(ctx, GitBashExecutor.Config(config)),
26
+ "wsl-bash": (ctx, config) => new WslBashExecutor(ctx, WslBashExecutor.Config(config)),
27
+ pwsh: (ctx, config) => new SandboxPwshExecutor(ctx, SandboxPwshExecutor.Config(config))
28
+ };
29
+ /**
30
+ * Selector executor over the bash capability seam. Registers as `ctx.shell`
31
+ * (the single seat), routes every request to one enabled backend by name, and
32
+ * stamps the chosen name on the resolved spec as `shellBackend`. Enabled
33
+ * names without a factory fail loud at the first rebuild; routing to an
34
+ * unbuilt name throws `SHELL_BACKEND_UNAVAILABLE`.
35
+ */
36
+ var ShellSelectExecutor = class ShellSelectExecutor extends ShellExecutor {
37
+ static inject = [
38
+ "subprocess",
39
+ "sandbox",
40
+ "sandboxPolicy"
41
+ ];
42
+ static Config = z.object({
43
+ backends: z.array(z.string()).default([
44
+ "git-bash",
45
+ "wsl-bash",
46
+ "pwsh"
47
+ ]),
48
+ default: z.string().default("pwsh"),
49
+ gitBash: GitBashExecutor.Config.default({}),
50
+ wslBash: WslBashExecutor.Config.default({}),
51
+ pwsh: SandboxPwshExecutor.Config.default({})
52
+ });
53
+ /** Test hook: replace backend construction wholesale (mirrors the sandbox-local internals pattern). */
54
+ internals = {};
55
+ /** The currently authoritative config: the settings section, or the composition entry. */
56
+ source;
57
+ backends = /* @__PURE__ */ new Map();
58
+ fibers = [];
59
+ constructor(ctx, config) {
60
+ super(ctx);
61
+ const entry = config;
62
+ assertServiceableBashConfig(entry.gitBash);
63
+ assertServiceableBashConfig(entry.wslBash);
64
+ assertServiceablePwshConfig(entry.pwsh);
65
+ this.source = () => entry;
66
+ installSettingsSection(ctx, SHELL_SETTINGS_NAMESPACE, ShellSelectExecutor.Config, entry, {
67
+ validate: (value) => {
68
+ const v = value;
69
+ assertServiceableBashConfig(v.gitBash);
70
+ assertServiceableBashConfig(v.wslBash);
71
+ assertServiceablePwshConfig(v.pwsh);
72
+ },
73
+ setSource: (current) => {
74
+ this.source = current;
75
+ },
76
+ onChange: () => {
77
+ this.rebuildBackends(this.config);
78
+ }
79
+ });
80
+ }
81
+ /** Validated config (schemastery applied the defaults before construction). */
82
+ get config() {
83
+ return this.source();
84
+ }
85
+ /**
86
+ * The first sandbox mode any enabled backend declares (tool-layer
87
+ * advertisement). One backend's probe/verdict failure (e.g. explicit
88
+ * `sandbox: bwrap` without bubblewrap installed) must not brick the whole
89
+ * selector: that backend advertises no mode here and its error resurfaces
90
+ * loudly at its first routed command — the documented lazy contract.
91
+ */
92
+ get sandboxMode() {
93
+ this.ensureBackends();
94
+ for (const name of this.config.backends) {
95
+ let mode;
96
+ try {
97
+ mode = this.backends.get(name)?.sandboxMode;
98
+ } catch {
99
+ continue;
100
+ }
101
+ if (mode !== void 0) return mode;
102
+ }
103
+ }
104
+ resolve(request) {
105
+ this.ensureBackends();
106
+ const name = request.shell ?? this.config.default;
107
+ return {
108
+ ...this.requireBackend(name).resolve(request),
109
+ shellBackend: name
110
+ };
111
+ }
112
+ run(spec) {
113
+ this.ensureBackends();
114
+ return this.requireBackend(spec.shellBackend ?? this.config.default).run(spec);
115
+ }
116
+ start(spec) {
117
+ this.ensureBackends();
118
+ return this.requireBackend(spec.shellBackend ?? this.config.default).start(spec);
119
+ }
120
+ requireBackend(name) {
121
+ const backend = this.backends.get(name);
122
+ if (backend === void 0) throw new HarnessError(`shell-select: backend "${name}" is not enabled (enabled: ${[...this.backends.keys()].join(", ")})`, SHELL_BACKEND_UNAVAILABLE);
123
+ return backend;
124
+ }
125
+ /** The first rebuild runs at first use, after the internals hook is installed. */
126
+ ensureBackends() {
127
+ if (this.backends.size > 0) return;
128
+ this.rebuildBackends(this.config);
129
+ }
130
+ /**
131
+ * Construct one backend per enabled name on its own child fiber with
132
+ * isolated shell/settings scopes (see the class doc), or throw for an
133
+ * unknown name. Old fibers are disposed first so rebuilds never leak.
134
+ */
135
+ rebuildBackends(entry) {
136
+ for (const fiber of this.fibers.splice(0)) fiber.dispose();
137
+ this.backends.clear();
138
+ for (const name of entry.backends) {
139
+ const factory = this.internals.factories?.[name] ?? DEFAULT_FACTORIES[name];
140
+ if (factory === void 0) throw new Error(`shell-select: unknown backend "${name}" (enabled: ${entry.backends.join(", ")})`);
141
+ const fiber = this.ctx.plugin({
142
+ inject: [
143
+ "subprocess",
144
+ "sandbox",
145
+ "sandboxPolicy"
146
+ ],
147
+ apply: () => {}
148
+ });
149
+ this.fibers.push(fiber);
150
+ const backendCtx = fiber.ctx.isolate("shell", Symbol(name)).isolate("settings", Symbol(name));
151
+ this.backends.set(name, factory(backendCtx, backendConfig(name, entry)));
152
+ }
153
+ }
154
+ };
155
+ /**
156
+ * The config partition for one backend name. Names without a dedicated
157
+ * partition (test-hook factories) receive an empty config; the owning factory
158
+ * decides what it needs.
159
+ */
160
+ function backendConfig(name, entry) {
161
+ switch (name) {
162
+ case "git-bash": return entry.gitBash;
163
+ case "wsl-bash": return entry.wslBash;
164
+ case "pwsh": return entry.pwsh;
165
+ default: return {};
166
+ }
167
+ }
168
+ //#endregion
169
+ export { SHELL_BACKEND_UNAVAILABLE, ShellSelectExecutor, ShellSelectExecutor as default };
@@ -0,0 +1,23 @@
1
+ //#region lib/types/invariant.js
2
+ /**
3
+ * Package-owned invariant companion for `@deepseek-ai/dsh-shell-select`.
4
+ * @module @deepseek-ai/dsh-shell-select/invariant
5
+ */
6
+ const PACKAGE_NAME = "@deepseek-ai/dsh-shell-select";
7
+ /** Cordis companion plugin name. */
8
+ const name = "shell-select-invariant";
9
+ /** Service required before the companion can reserve package ownership. */
10
+ const inject = ["invariants"];
11
+ /**
12
+ * No runtime invariant: this package exposes no independent event sequence or
13
+ * mutable data relation beyond contracts enforced at its owning seams.
14
+ */
15
+ const install = () => {};
16
+ /**
17
+ * Register this package's invariant companion.
18
+ * @param ctx - Cordis context carrying the invariant service.
19
+ * @returns the installed registration's disposer after setup succeeds.
20
+ */
21
+ const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
22
+ //#endregion
23
+ export { apply, inject, name };