dsh-bash-terminal-ts 0.4.0 → 0.6.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.
- package/README.en.md +53 -162
- package/README.md +103 -94
- package/cordis.patch.yml +2 -2
- package/dist/client.core.js +49 -42
- package/dist/client.js +50 -43
- package/dsh.plugin.json +4 -4
- package/lib/client.js +148 -0
- package/lib/dsh-types.d.ts +1 -2
- package/lib/dsh.d.ts +2 -0
- package/lib/index.d.ts +17 -18
- package/lib/index.js +63 -57
- package/lib/terminal.d.ts +24 -1
- package/lib/terminal.js +214 -49
- package/package.json +115 -137
- package/screenshots.json +5 -0
- package/scripts/build-client.mjs +14 -13
- package/src/client.tsx +80 -55
- package/src/dsh-types.ts +1 -2
- package/src/dsh.ts +2 -0
- package/src/index.ts +67 -61
- package/src/terminal.ts +254 -54
- package/template/README.md +82 -0
- package/template/terminal-adapter.ts +343 -0
- package/README.ja.md +0 -196
- package/README.ko.md +0 -194
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// dsh-bash-terminal
|
|
1
|
+
// dsh-bash-terminal - one shell tool, four Windows terminals.
|
|
2
2
|
//
|
|
3
3
|
// Registers a model-facing shell tool. The terminal backend (powershell /
|
|
4
4
|
// gitbash / msys2 / wsl) is chosen by the USER in the Web UI settings (default
|
|
@@ -6,11 +6,7 @@
|
|
|
6
6
|
// choice:
|
|
7
7
|
// - powershell: pwsh -NoLogo -NoProfile -NonInteractive -Command <cmd>
|
|
8
8
|
// - gitbash: Git for Windows bash -lc <cmd> (POSIX; /d/... paths)
|
|
9
|
-
// - msys2:
|
|
10
|
-
// toolchain). NOT msys2.exe: that Cygwin launcher allocates a
|
|
11
|
-
// console and returns exit 0 with zero bytes under piped stdio.
|
|
12
|
-
// MSYSTEM=MINGW64 is injected via buildEnv so /etc/profile picks
|
|
13
|
-
// the MINGW64 environment and /mingw64/bin lands on PATH.
|
|
9
|
+
// - msys2: MSYS2 bash -lc <cmd> (POSIX; /c/... paths, full GCC/mingw64 toolchain)
|
|
14
10
|
// - wsl: wsl [-d <distro>] -e bash -lc <cmd> (Linux; /mnt/d/... paths)
|
|
15
11
|
//
|
|
16
12
|
// The tool spawns through the shared ctx.subprocess seam (process-tree
|
|
@@ -18,8 +14,13 @@
|
|
|
18
14
|
// handles with the generic ctx.jobs registry, mirroring the shipped
|
|
19
15
|
// dsh-tool-bash / dsh-tool-pwsh story call-for-call. It deliberately
|
|
20
16
|
// does NOT consume the ctx.shell capability seam: the platform's own
|
|
21
|
-
// sandboxed PowerShell executor keeps serving the pwsh tool
|
|
22
|
-
//
|
|
17
|
+
// sandboxed PowerShell executor keeps serving the pwsh tool. This tool is
|
|
18
|
+
// an additional, user-selected terminal that still honors the DSH sandbox:
|
|
19
|
+
// confined modes wrap PowerShell's spawn argv through ctx.sandbox
|
|
20
|
+
// (fail-closed), while danger-full-access, Git Bash, MSYS2, and WSL run
|
|
21
|
+
// unconfined. Git Bash and MSYS2 are unconfined because the DSH Windows ACL
|
|
22
|
+
// restricted-token runner cannot host Cygwin/MSYS2: bash aborts with
|
|
23
|
+
// "CreateFileMapping ... Win32 error 5" during startup.
|
|
23
24
|
|
|
24
25
|
import { lstatSync } from "node:fs";
|
|
25
26
|
import { isAbsolute, join, resolve } from "node:path";
|
|
@@ -60,8 +61,6 @@ export const inject = ["tools", "systemPrompt", "shellEnv", "subprocess", "setti
|
|
|
60
61
|
export const SHELLS = ["powershell", "gitbash", "msys2", "wsl"] as const;
|
|
61
62
|
/** The backend used when the caller does not name one. */
|
|
62
63
|
export const DEFAULT_SHELL: ShellId = "powershell";
|
|
63
|
-
/** Settings namespace backing the user-chosen default terminal. */
|
|
64
|
-
export const SETTINGS_NAMESPACE = "bash-terminal";
|
|
65
64
|
/** Default per-command timeout (ms). */
|
|
66
65
|
const DEFAULT_TIMEOUT_MS = 120000;
|
|
67
66
|
/** Upper bound a caller's timeoutMs is capped to. */
|
|
@@ -83,6 +82,11 @@ const ENV_OVERRIDES: Record<string, string> = {
|
|
|
83
82
|
GIT_PAGER: "cat"
|
|
84
83
|
};
|
|
85
84
|
|
|
85
|
+
/** Live reference the 0.1.7 loader hands `apply` for `.volatile()` config fields. */
|
|
86
|
+
interface VolatileRef<T> {
|
|
87
|
+
get(): T;
|
|
88
|
+
}
|
|
89
|
+
|
|
86
90
|
/** Static shape of the runtime configuration schema. */
|
|
87
91
|
export interface ConfigValues {
|
|
88
92
|
defaultShell: string;
|
|
@@ -96,7 +100,7 @@ export interface ConfigValues {
|
|
|
96
100
|
|
|
97
101
|
/** Runtime configuration schema. */
|
|
98
102
|
export const Config = z.object({
|
|
99
|
-
defaultShell: z.string().default(DEFAULT_SHELL),
|
|
103
|
+
defaultShell: z.string().default(DEFAULT_SHELL).volatile(),
|
|
100
104
|
timeoutMs: z.number().default(DEFAULT_TIMEOUT_MS),
|
|
101
105
|
maxTimeoutMs: z.number().default(MAX_TIMEOUT_MS),
|
|
102
106
|
pwshPath: z.string().default(""),
|
|
@@ -161,33 +165,29 @@ export function candidateGitBashPaths(env: NodeJS.ProcessEnv = process.env): str
|
|
|
161
165
|
}
|
|
162
166
|
|
|
163
167
|
/**
|
|
164
|
-
* MSYS2 locations, in preference order: the real `bash.exe` under usr\bin
|
|
165
|
-
* then bin\bash.exe, and `msys2.exe` dead last.
|
|
168
|
+
* MSYS2 locations, in preference order: the real `bash.exe` under usr\bin
|
|
169
|
+
* first, then bin\bash.exe, and `msys2.exe` dead last.
|
|
166
170
|
*
|
|
167
171
|
* msys2.exe is NOT a usable backend for piped execution: it is the console-
|
|
168
172
|
* allocating Cygwin launcher, so a spawn with piped stdio returns exit 0 with
|
|
169
|
-
* zero bytes on both stdout and stderr (measured on
|
|
170
|
-
* Keeping it in the list only as a last-resort
|
|
171
|
-
* config docs reference, but a working
|
|
173
|
+
* zero bytes on both stdout and stderr (measured on this machine against
|
|
174
|
+
* MSYS2 with bash 5.3.15). Keeping it in the list only as a last-resort
|
|
175
|
+
* fallback preserves the path the config docs reference, but a working
|
|
176
|
+
* bash.exe always wins.
|
|
172
177
|
*
|
|
173
|
-
* MSYS2 uses the same Cygwin/MSYS2 runtime as Git Bash, so it cannot run
|
|
174
|
-
* the DSH Windows ACL restricted-token sandbox.
|
|
178
|
+
* MSYS2 uses the same Cygwin/MSYS2 runtime as Git Bash, so it cannot run
|
|
179
|
+
* under the DSH Windows ACL restricted-token sandbox.
|
|
175
180
|
*/
|
|
176
181
|
export function candidateMsys2Paths(env: NodeJS.ProcessEnv = process.env): string[] {
|
|
177
|
-
const programFiles = env.ProgramFiles ?? "C:\\Program Files";
|
|
178
182
|
const candidates = [
|
|
179
183
|
"C:\\msys64\\usr\\bin\\bash.exe",
|
|
180
184
|
"C:\\msys64\\bin\\bash.exe"
|
|
181
185
|
];
|
|
182
|
-
// Also check 32-bit variant
|
|
183
|
-
const programFilesX86 = env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)";
|
|
184
|
-
if (programFilesX86 !== programFiles) {
|
|
185
|
-
candidates.push("C:\\msys64\\usr\\bin\\bash.exe"); // same path regardless of arch
|
|
186
|
-
}
|
|
187
186
|
for (const entry of (env.PATH ?? "").split(";")) {
|
|
188
187
|
const trimmed = entry.trim().replace(/^"|"$/g, "");
|
|
189
188
|
if (trimmed.length === 0) continue;
|
|
190
|
-
|
|
189
|
+
const lower = trimmed.toLowerCase();
|
|
190
|
+
if (lower.includes("msys64") || lower.includes("mingw64")) {
|
|
191
191
|
candidates.push(join(trimmed, "bash.exe"));
|
|
192
192
|
}
|
|
193
193
|
}
|
|
@@ -250,16 +250,16 @@ export function buildArgv(
|
|
|
250
250
|
|
|
251
251
|
/**
|
|
252
252
|
* Merge the DSH_* environment over the process environment. For WSL, only
|
|
253
|
-
* variables explicitly listed in WSLENV cross the boundary, so every DSH_*
|
|
254
|
-
* appended
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
* rebuilt, or those entries would be silently dropped from every WSL call.
|
|
259
|
-
* Callers that spawn through a seam which replaces the parent environment
|
|
260
|
-
* wholesale (the PTY path) must pass the inherited value explicitly.
|
|
253
|
+
* variables explicitly listed in WSLENV cross the boundary, so every DSH_*
|
|
254
|
+
* key is appended there — and the list is *layered onto* whatever WSLENV the
|
|
255
|
+
* host already had (Windows Terminal exports e.g. `WT_SESSION:WT_PROFILE_ID:`),
|
|
256
|
+
* never rebuilt from scratch: WSLENV is an allow-list, so dropping the
|
|
257
|
+
* inherited entries would silently stop them crossing into WSL.
|
|
261
258
|
*
|
|
262
|
-
* @param inheritedWslenv - the WSLENV
|
|
259
|
+
* @param inheritedWslenv - the ambient WSLENV to layer onto. Callers that
|
|
260
|
+
* replace the child environment wholesale (the PTY path) must pass
|
|
261
|
+
* `process.env.WSLENV` explicitly, because the ambient value is not visible
|
|
262
|
+
* through `dshEnv`.
|
|
263
263
|
*/
|
|
264
264
|
export function buildEnv(
|
|
265
265
|
shell: string,
|
|
@@ -268,25 +268,27 @@ export function buildEnv(
|
|
|
268
268
|
): Record<string, string | undefined> {
|
|
269
269
|
const env: Record<string, string | undefined> = { ...ENV_OVERRIDES, ...dshEnv };
|
|
270
270
|
if (shell === "msys2") {
|
|
271
|
-
// Picks the MINGW64 environment, so /mingw64/bin (gcc, make, ...) joins
|
|
272
|
-
// via /etc/profile. Without it MSYS2 defaults to the bare MSYS
|
|
271
|
+
// Picks the MINGW64 environment, so /mingw64/bin (gcc, make, ...) joins
|
|
272
|
+
// PATH via /etc/profile. Without it MSYS2 defaults to the bare MSYS
|
|
273
|
+
// environment. An explicit caller-supplied value wins.
|
|
273
274
|
if (env.MSYSTEM === undefined) env.MSYSTEM = "MINGW64";
|
|
274
275
|
}
|
|
275
276
|
if (shell === "wsl") {
|
|
276
277
|
const keys = Object.keys(dshEnv ?? {});
|
|
277
278
|
if (keys.length > 0) {
|
|
278
|
-
// An explicit WSLENV
|
|
279
|
+
// An explicit WSLENV in dshEnv wins over the inherited one; otherwise the
|
|
280
|
+
// inherited value is the base. Either way the base is SPLIT into entries
|
|
281
|
+
// (the host value ends with a trailing ":", so string concatenation would
|
|
282
|
+
// produce a malformed empty entry) and the WSLENV key itself is excluded
|
|
283
|
+
// (it is a key of dshEnv too, and appending it would add a bogus entry).
|
|
279
284
|
const declared = Object.prototype.hasOwnProperty.call(dshEnv ?? {}, "WSLENV");
|
|
280
285
|
const base = declared
|
|
281
286
|
? env.WSLENV
|
|
282
287
|
: (typeof inheritedWslenv === "string" && inheritedWslenv.length > 0 ? inheritedWslenv : env.WSLENV);
|
|
283
|
-
// Windows Terminal's own value ends with a trailing ':' and the caller's
|
|
284
|
-
// may too, so split/normalise instead of string-concatenating -- a naive
|
|
285
|
-
// join yields an empty entry ("A:B::DSH_X"), which WSL flags as malformed.
|
|
286
288
|
const parts = typeof base === "string" ? base.split(":") : [];
|
|
287
289
|
env.WSLENV = [...parts, ...keys.filter((k) => k !== "WSLENV").flatMap((k) => k.split(":"))]
|
|
288
|
-
.map((
|
|
289
|
-
.filter((
|
|
290
|
+
.map((p) => p.trim())
|
|
291
|
+
.filter((p) => p.length > 0)
|
|
290
292
|
.join(":");
|
|
291
293
|
}
|
|
292
294
|
}
|
|
@@ -322,7 +324,7 @@ function spawnSpec(resolved: SpawnResolution, argv: string[], env: Record<string
|
|
|
322
324
|
function collectedOutput(handle: { collected: { stdout: unknown; stderr: unknown } }) {
|
|
323
325
|
const { stdout, stderr } = handle.collected;
|
|
324
326
|
if (!isCollectedStream(stdout) || !isCollectedStream(stderr)) {
|
|
325
|
-
throw new Error("dsh-bash-terminal
|
|
327
|
+
throw new Error("dsh-bash-terminal: subprocess implementation dropped a requested collect stream");
|
|
326
328
|
}
|
|
327
329
|
return { stdout, stderr };
|
|
328
330
|
}
|
|
@@ -393,7 +395,7 @@ async function runForeground(
|
|
|
393
395
|
function requireSubprocess(ctx: BashTerminalContext) {
|
|
394
396
|
const subprocess = ctx.subprocess;
|
|
395
397
|
if (subprocess === null) {
|
|
396
|
-
throw new Error("dsh-bash-terminal
|
|
398
|
+
throw new Error("dsh-bash-terminal: ctx.subprocess seam unavailable (missing inject service)");
|
|
397
399
|
}
|
|
398
400
|
return subprocess;
|
|
399
401
|
}
|
|
@@ -586,8 +588,9 @@ export function validateArgs(args: Record<string, unknown>): ValidatedShellArgs
|
|
|
586
588
|
* Git Bash and MSYS2 are also not confined: DSH's Windows ACL restricted-token
|
|
587
589
|
* runner cannot start Cygwin/MSYS2 (CreateFileMapping Win32 error 5), so
|
|
588
590
|
* attempting to wrap them would abort every Git Bash / MSYS2 command. A
|
|
589
|
-
* requested confined mode
|
|
590
|
-
*
|
|
591
|
+
* requested confined mode
|
|
592
|
+
* with no usable backend throws the fail-closed SandboxUnavailableError,
|
|
593
|
+
* exactly like the shipped executors.
|
|
591
594
|
*/
|
|
592
595
|
function confineSpawn(
|
|
593
596
|
ctx: BashTerminalContext,
|
|
@@ -666,20 +669,23 @@ const BACKGROUND_OUTPUT_PROPERTIES: Record<string, JsonSchemaNode> = {
|
|
|
666
669
|
|
|
667
670
|
export function apply(ctx: BashTerminalContext, config: Partial<ConfigValues> = {}): void {
|
|
668
671
|
if (process.platform !== "win32") {
|
|
669
|
-
ctx.logger?.info?.("dsh-bash-terminal
|
|
672
|
+
ctx.logger?.info?.("dsh-bash-terminal: only meaningful on win32; skipping tool registration");
|
|
670
673
|
return;
|
|
671
674
|
}
|
|
672
675
|
const backgroundEnabled = true;
|
|
673
676
|
const paths = resolveAllPaths(config);
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
+
// 0.1.7: `defaultShell` is a `.volatile()` config field — the loader hands
|
|
678
|
+
// `apply` a live ref and the latest value is resolved on every read; there
|
|
679
|
+
// is no settings registration call anymore (removed with the 0.1.7 host).
|
|
680
|
+
const currentShell = (): string => {
|
|
681
|
+
const v = config.defaultShell as string | VolatileRef<string> | undefined;
|
|
682
|
+
if (typeof v === "string") return v;
|
|
683
|
+
const got = typeof v?.get === "function" ? v.get() : undefined;
|
|
684
|
+
return typeof got === "string" ? got : DEFAULT_SHELL;
|
|
685
|
+
};
|
|
686
|
+
if (!(SHELLS as readonly string[]).includes(currentShell())) {
|
|
687
|
+
throw new Error(`dsh-bash-terminal: invalid defaultShell ${JSON.stringify(currentShell())}`);
|
|
677
688
|
}
|
|
678
|
-
const settingsScope = ctx.settings.register(
|
|
679
|
-
SETTINGS_NAMESPACE,
|
|
680
|
-
z.object({ defaultShell: z.union(SHELLS.map((s) => z.const(s))).default(defaultShell) }),
|
|
681
|
-
{ base: { defaultShell } }
|
|
682
|
-
);
|
|
683
689
|
/** Official sandbox-escalation surface (mirrors tool-bash): advertise the
|
|
684
690
|
* escalation modes whenever the deployment confines. */
|
|
685
691
|
const escalationModes = ESCALATION_TARGETS;
|
|
@@ -706,12 +712,12 @@ export function apply(ctx: BashTerminalContext, config: Partial<ConfigValues> =
|
|
|
706
712
|
ctx.systemPrompt.section({
|
|
707
713
|
name: "tool:bash-terminal",
|
|
708
714
|
order: 105,
|
|
709
|
-
text: "Use the shell tool for terminal commands: it runs in the terminal the user chose in Settings -> General -> Default terminal (PowerShell, Git Bash, or WSL) and honors the DSH sandbox. Prefer it over the pwsh tool for everyday commands; keep the pwsh tool for cases that specifically need the sandboxed PowerShell surface."
|
|
715
|
+
text: "Use the shell tool for terminal commands: it runs in the terminal the user chose in Settings -> General -> Default terminal (PowerShell, Git Bash, MSYS2, or WSL) and honors the DSH sandbox. Prefer it over the pwsh tool for everyday commands; keep the pwsh tool for cases that specifically need the sandboxed PowerShell surface."
|
|
710
716
|
});
|
|
711
717
|
|
|
712
718
|
const toolName = "shell";
|
|
713
719
|
const terminalRegistry = createTerminalRegistry(ctx);
|
|
714
|
-
ctx.tools.register(terminalTool(ctx, terminalRegistry, paths, () =>
|
|
720
|
+
ctx.tools.register(terminalTool(ctx, terminalRegistry, paths, () => currentShell()));
|
|
715
721
|
|
|
716
722
|
// The model-facing description must track the user's chosen default
|
|
717
723
|
// terminal: a static description listing every backend leaves the model
|
|
@@ -720,7 +726,7 @@ export function apply(ctx: BashTerminalContext, config: Partial<ConfigValues> =
|
|
|
720
726
|
// without a restart.
|
|
721
727
|
ctx.on("system-prompt/assemble", async (_assembly, _context, next) => {
|
|
722
728
|
const assembled = await next();
|
|
723
|
-
const description = toolDescription(backgroundEnabled,
|
|
729
|
+
const description = toolDescription(backgroundEnabled, currentShell());
|
|
724
730
|
const tools = Array.isArray(assembled.tools)
|
|
725
731
|
? assembled.tools.map((tool) => (tool.name === toolName ? { ...tool, description } : tool))
|
|
726
732
|
: assembled.tools;
|
|
@@ -729,7 +735,7 @@ export function apply(ctx: BashTerminalContext, config: Partial<ConfigValues> =
|
|
|
729
735
|
|
|
730
736
|
ctx.tools.register(defineTool<ShellToolResult>({
|
|
731
737
|
name: toolName,
|
|
732
|
-
description: toolDescription(backgroundEnabled,
|
|
738
|
+
description: toolDescription(backgroundEnabled, currentShell()),
|
|
733
739
|
parameters: {
|
|
734
740
|
command: {
|
|
735
741
|
type: "string",
|
|
@@ -826,10 +832,10 @@ export function apply(ctx: BashTerminalContext, config: Partial<ConfigValues> =
|
|
|
826
832
|
},
|
|
827
833
|
async execute(args, exec) {
|
|
828
834
|
const v = validateArgs(args);
|
|
829
|
-
const shell =
|
|
835
|
+
const shell = currentShell();
|
|
830
836
|
const argv0 = buildArgv(shell, v.command, paths, v.distro);
|
|
831
837
|
if (argv0[0] === undefined) {
|
|
832
|
-
throw new Error(`dsh-bash-terminal
|
|
838
|
+
throw new Error(`dsh-bash-terminal: ${shell} backend unavailable - executable not found. Install it or set the corresponding *Path config.`);
|
|
833
839
|
}
|
|
834
840
|
// Only element 0 (the resolved executable) can be undefined; guarded above.
|
|
835
841
|
const argv = argv0 as string[];
|