dsh-bash-terminal-ts 0.5.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 +7 -1
- package/README.md +14 -8
- package/dist/client.core.js +3 -3
- package/dist/client.js +3 -3
- package/lib/client.js +3 -3
- package/lib/dsh-types.d.ts +0 -1
- package/lib/dsh.d.ts +2 -0
- package/lib/index.d.ts +0 -2
- package/lib/index.js +17 -11
- package/package.json +115 -116
- package/screenshots.json +5 -3
- package/src/client.tsx +10 -9
- package/src/dsh-types.ts +0 -1
- package/src/dsh.ts +2 -0
- package/src/index.ts +21 -15
- package/template/README.md +82 -0
- package/template/terminal-adapter.ts +343 -0
package/README.en.md
CHANGED
|
@@ -20,7 +20,7 @@ A DeepSeek Harness (DSH) plugin: one `shell` tool that runs commands through **P
|
|
|
20
20
|
| Backend | Runs | Syntax / paths | Env vars |
|
|
21
21
|
|---------|------|----------------|----------|
|
|
22
22
|
| `powershell` (default) | `pwsh -NoLogo -NoProfile -NonInteractive -Command <cmd>` | PowerShell; `C:\\...` | `$env:NAME` |
|
|
23
|
-
| `gitbash` | Git for Windows `bash -lc <cmd>` | POSIX; `/d/
|
|
23
|
+
| `gitbash` | Git for Windows `bash -lc <cmd>` | POSIX; `/d/workspace`; PATH includes `/usr/bin` and `/mingw64/bin` | `$NAME` |
|
|
24
24
|
| `msys2` | MSYS2 `bash -lc <cmd>` (`C:\msys64\usr\bin\bash.exe`) | POSIX; `/c/...`; PATH includes `/usr/bin` and `/mingw64/bin` (gcc / make) | `$NAME` (MSYSTEM=MINGW64 injected) |
|
|
25
25
|
| `wsl` | `wsl [-d <distro>] -e bash -lc <cmd>` | Linux; `/mnt/d/...` | `$NAME` (via WSLENV) |
|
|
26
26
|
|
|
@@ -29,6 +29,12 @@ A DeepSeek Harness (DSH) plugin: one `shell` tool that runs commands through **P
|
|
|
29
29
|
- **Interactive terminal**: the `terminal` tool opens persistent real-PTY sessions over node-pty — on non-Windows via the official `ctx.subprocess.spawnTerminal` seam, on Windows directly through node-pty because the upstream seam's process inspector is POSIX-only. Actions `open` / `send` / `read` / `signal` / `close`; shell state persists across calls; sessions are managed as background jobs and auto-close when idle.
|
|
30
30
|
- **Background execution** via the generic jobs registry (`run_in_background` / `job_output` / `job_kill`).
|
|
31
31
|
|
|
32
|
+
## Screenshots
|
|
33
|
+
|
|
34
|
+
The **Default terminal** row in Settings -> General: the user picks PowerShell / Git Bash / MSYS2 / WSL, and the `shell` tool obeys that choice — the model cannot override it.
|
|
35
|
+
|
|
36
|
+

|
|
37
|
+
|
|
32
38
|
## Install
|
|
33
39
|
|
|
34
40
|
The package ships the official `dsh.bundle` manifest (its own `cordis.patch.yml`): listing `dsh-bash-terminal` in a profile's `dsh.profile.bundles` auto-applies the mount — no manual profile edits.
|
package/README.md
CHANGED
|
@@ -18,12 +18,18 @@ DSH(DeepSeek Harness)插件:一个 `shell` 工具,在 Windows 上统一
|
|
|
18
18
|
| 后端 | 实际执行 | 语法 / 路径 | 环境变量 |
|
|
19
19
|
|------|----------|-------------|----------|
|
|
20
20
|
| `powershell`(默认) | `pwsh -NoLogo -NoProfile -NonInteractive -Command <cmd>` | PowerShell;`C:\...` | `$env:NAME` |
|
|
21
|
-
| `gitbash` | Git for Windows `bash -lc <cmd>` | POSIX;`/d/
|
|
21
|
+
| `gitbash` | Git for Windows `bash -lc <cmd>` | POSIX;`/d/workspace`;PATH 含 `/usr/bin`、`/mingw64/bin` | `$NAME` |
|
|
22
22
|
| `msys2` | MSYS2 `bash -lc <cmd>`(`C:\msys64\usr\bin\bash.exe`) | POSIX;`/c/...`;PATH 含 `/usr/bin`、`/mingw64/bin`(gcc / make) | `$NAME`(自动注入 `MSYSTEM=MINGW64`) |
|
|
23
23
|
| `wsl` | `wsl [-d <distro>] -e bash -lc <cmd>` | Linux;`/mnt/d/...` | `$NAME`(经 WSLENV) |
|
|
24
24
|
|
|
25
25
|
每次调用都启动全新 shell:**不保留状态**(cwd / 变量 / 别名)——请传 `workdir` 而不是用 `cd`。
|
|
26
26
|
|
|
27
|
+
## 界面预览
|
|
28
|
+
|
|
29
|
+
设置 → 通用 里的「默认终端」行:用户在 PowerShell / Git Bash / MSYS2 / WSL 之间选择,`shell` 工具只按这个设置执行,模型无法覆盖:
|
|
30
|
+
|
|
31
|
+

|
|
32
|
+
|
|
27
33
|
## 设计要点
|
|
28
34
|
|
|
29
35
|
- **终端由用户决定,AI 无法更改**:Web UI 设置页(设置 → 通用)出现"默认终端"下拉(PowerShell / Git Bash / MSYS2 / WSL);`shell` 工具永远只使用该设置,不暴露终端参数给模型。设置通过 DSH settings 系统持久化(settings.yaml)。
|
|
@@ -56,14 +62,14 @@ powershell -ExecutionPolicy Bypass -File install.ps1 install
|
|
|
56
62
|
```powershell
|
|
57
63
|
# 1. 链接插件包到 profile 的 node_modules(junction,改源码即时生效)
|
|
58
64
|
$profile = "$env:USERPROFILE\.dsh\profiles\web"
|
|
59
|
-
New-Item -ItemType Junction -Path "$profile\node_modules\dsh-bash-terminal" -Target "D:\
|
|
65
|
+
New-Item -ItemType Junction -Path "$profile\node_modules\dsh-bash-terminal" -Target "D:\workspace\projects\dsh-bash-terminal" | Out-Null
|
|
60
66
|
|
|
61
67
|
# 2. 让插件能解析 @deepseek-ai/* 依赖(junction 到 profile 的依赖树,插件与宿主共用同一份模块实例)
|
|
62
|
-
New-Item -ItemType Junction -Path "D:\
|
|
68
|
+
New-Item -ItemType Junction -Path "D:\workspace\projects\dsh-bash-terminal\node_modules\@deepseek-ai" -Target "$profile\..\node_modules\@deepseek-ai" | Out-Null
|
|
63
69
|
|
|
64
70
|
# 3. 让 profile 通过官方 bundle 挂载插件(install.ps1 install 会自动做;等价于在 dsh.profile.bundles 加 "dsh-bash-terminal")
|
|
65
71
|
# 4. (仅修改前端源码后)重新打包 client bundle:
|
|
66
|
-
# cd D:\
|
|
72
|
+
# cd D:\workspace\projects\dsh-bash-terminal && node scripts/build-client.mjs
|
|
67
73
|
# 5. 重启 dsh web
|
|
68
74
|
```
|
|
69
75
|
|
|
@@ -83,7 +89,7 @@ New-Item -ItemType Junction -Path "D:\WorkSpace\projects\dsh-bash-terminal\node_
|
|
|
83
89
|
验证组合树(无需重启):
|
|
84
90
|
|
|
85
91
|
```powershell
|
|
86
|
-
node "$env:APPDATA\nvm
|
|
92
|
+
node "$env:APPDATA\nvm\<node-version>\node_modules\@deepseek-ai\dsh\lib\bin.js" --profile web --dump-config | Select-String dsh-bash-terminal
|
|
87
93
|
```
|
|
88
94
|
|
|
89
95
|
## 使用
|
|
@@ -94,7 +100,7 @@ node "$env:APPDATA\nvm\v24.16.0\node_modules\@deepseek-ai\dsh\lib\bin.js" --prof
|
|
|
94
100
|
|
|
95
101
|
- 默认终端 = Git Bash 时:`shell(command: "git status")` 走 Git Bash
|
|
96
102
|
- 默认终端 = MSYS2 时:`shell(command: "gcc --version")` 走 MSYS2(MINGW64 环境,`/mingw64/bin` 的 gcc、make 可用)
|
|
97
|
-
- 默认终端 = WSL 时:`shell(command: "ls -la /mnt/d/
|
|
103
|
+
- 默认终端 = WSL 时:`shell(command: "ls -la /mnt/d/workspace")` 走 WSL;传 `distro: "Ubuntu"` 可指定发行版
|
|
98
104
|
- 默认终端 = PowerShell 时:`shell(command: "Get-Process node")` 走 PowerShell
|
|
99
105
|
|
|
100
106
|
## 模型使用示例
|
|
@@ -126,7 +132,7 @@ node "$env:APPDATA\nvm\v24.16.0\node_modules\@deepseek-ai\dsh\lib\bin.js" --prof
|
|
|
126
132
|
npm 账号已启用 2FA 发布验证,需一次性验证码:
|
|
127
133
|
|
|
128
134
|
```powershell
|
|
129
|
-
cd D:\
|
|
135
|
+
cd D:\workspace\projects\dsh-bash-terminal
|
|
130
136
|
npm publish --otp <验证码> # 验证码来自你的认证器
|
|
131
137
|
```
|
|
132
138
|
|
|
@@ -188,7 +194,7 @@ powershell -ExecutionPolicy Bypass -File install.ps1 uninstall
|
|
|
188
194
|
## 测试
|
|
189
195
|
|
|
190
196
|
```powershell
|
|
191
|
-
cd D:\
|
|
197
|
+
cd D:\workspace\projects\dsh-bash-terminal
|
|
192
198
|
npm install # 安装依赖(含 typescript)
|
|
193
199
|
npm run build # tsc 编译 src/*.ts → lib/*.js;client.tsx → lib/client.js + dist/client.js;test/*.ts → test-dist/
|
|
194
200
|
npm test # node test-dist/unit.js → apply.js → client.js → terminal.js
|
package/dist/client.core.js
CHANGED
|
@@ -28,7 +28,7 @@ var import_dsh_client_store = require("@deepseek-ai/dsh-client-store");
|
|
|
28
28
|
var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
29
29
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
30
30
|
var SETTINGS_NS = "settings.bash-terminal";
|
|
31
|
-
var
|
|
31
|
+
var ENTRY_ID = "tool-bash-terminal";
|
|
32
32
|
var SHELLS = ["powershell", "gitbash", "msys2", "wsl"];
|
|
33
33
|
var ROW_CSS = ".btRow{border-bottom:1px solid var(--dsw-alias-border-l2);align-items:center;gap:8px;padding:16px 0;display:flex}.btRowText{flex-direction:column;flex:1;gap:4px;min-width:0;padding-right:48px;display:flex}.btTitle{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:22px}.btDesc{color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:400;line-height:18px}.btSelector{background:var(--dsw-alias-bg-module-platform);height:36px;font:inherit;color:var(--dsw-alias-label-primary);cursor:pointer;border:none;border-radius:18px;align-items:center;gap:12px;padding:0 14px;font-size:14px;line-height:22px;display:inline-flex}.btSelector:hover{background:var(--dsw-alias-interactive-bg-hover)}.btChevron{flex:none}";
|
|
34
34
|
if (typeof document !== "undefined" && document.querySelector('style[data-plugin-css="bash-terminal-row"]') === null) {
|
|
@@ -54,7 +54,7 @@ var en = {
|
|
|
54
54
|
"shell.msys2": "MSYS2",
|
|
55
55
|
"shell.wsl": "WSL"
|
|
56
56
|
};
|
|
57
|
-
var inject = ["slots", "locale", "
|
|
57
|
+
var inject = ["slots", "locale", "configForms"];
|
|
58
58
|
function ShellPreferenceRow({ t, useStore, setShell }) {
|
|
59
59
|
const shell = useStore((s) => s.shell);
|
|
60
60
|
const writable = useStore((s) => s.writable);
|
|
@@ -100,7 +100,7 @@ function ShellPreferenceRow({ t, useStore, setShell }) {
|
|
|
100
100
|
}
|
|
101
101
|
function apply(ctx) {
|
|
102
102
|
ctx.effect(() => ctx.locale.register(SETTINGS_NS, { zh, en }), "bash-terminal: settings dictionaries");
|
|
103
|
-
const scope = ctx.
|
|
103
|
+
const scope = ctx.configForms.get(ENTRY_ID);
|
|
104
104
|
const store = (0, import_dsh_client_store.defineStore)({
|
|
105
105
|
init: () => ({ shell: "powershell", revision: -1, writable: false }),
|
|
106
106
|
actions: {
|
package/dist/client.js
CHANGED
|
@@ -33,7 +33,7 @@ var import_dsh_client_store = require("@deepseek-ai/dsh-client-store");
|
|
|
33
33
|
var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
34
34
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
35
35
|
var SETTINGS_NS = "settings.bash-terminal";
|
|
36
|
-
var
|
|
36
|
+
var ENTRY_ID = "tool-bash-terminal";
|
|
37
37
|
var SHELLS = ["powershell", "gitbash", "msys2", "wsl"];
|
|
38
38
|
var ROW_CSS = ".btRow{border-bottom:1px solid var(--dsw-alias-border-l2);align-items:center;gap:8px;padding:16px 0;display:flex}.btRowText{flex-direction:column;flex:1;gap:4px;min-width:0;padding-right:48px;display:flex}.btTitle{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:22px}.btDesc{color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:400;line-height:18px}.btSelector{background:var(--dsw-alias-bg-module-platform);height:36px;font:inherit;color:var(--dsw-alias-label-primary);cursor:pointer;border:none;border-radius:18px;align-items:center;gap:12px;padding:0 14px;font-size:14px;line-height:22px;display:inline-flex}.btSelector:hover{background:var(--dsw-alias-interactive-bg-hover)}.btChevron{flex:none}";
|
|
39
39
|
if (typeof document !== "undefined" && document.querySelector('style[data-plugin-css="bash-terminal-row"]') === null) {
|
|
@@ -59,7 +59,7 @@ var en = {
|
|
|
59
59
|
"shell.msys2": "MSYS2",
|
|
60
60
|
"shell.wsl": "WSL"
|
|
61
61
|
};
|
|
62
|
-
var inject = ["slots", "locale", "
|
|
62
|
+
var inject = ["slots", "locale", "configForms"];
|
|
63
63
|
function ShellPreferenceRow({ t, useStore, setShell }) {
|
|
64
64
|
const shell = useStore((s) => s.shell);
|
|
65
65
|
const writable = useStore((s) => s.writable);
|
|
@@ -105,7 +105,7 @@ function ShellPreferenceRow({ t, useStore, setShell }) {
|
|
|
105
105
|
}
|
|
106
106
|
function apply(ctx) {
|
|
107
107
|
ctx.effect(() => ctx.locale.register(SETTINGS_NS, { zh, en }), "bash-terminal: settings dictionaries");
|
|
108
|
-
const scope = ctx.
|
|
108
|
+
const scope = ctx.configForms.get(ENTRY_ID);
|
|
109
109
|
const store = (0, import_dsh_client_store.defineStore)({
|
|
110
110
|
init: () => ({ shell: "powershell", revision: -1, writable: false }),
|
|
111
111
|
actions: {
|
package/lib/client.js
CHANGED
|
@@ -33,7 +33,7 @@ var import_dsh_client_store = require("@deepseek-ai/dsh-client-store");
|
|
|
33
33
|
var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
34
34
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
35
35
|
var SETTINGS_NS = "settings.bash-terminal";
|
|
36
|
-
var
|
|
36
|
+
var ENTRY_ID = "tool-bash-terminal";
|
|
37
37
|
var SHELLS = ["powershell", "gitbash", "msys2", "wsl"];
|
|
38
38
|
var ROW_CSS = ".btRow{border-bottom:1px solid var(--dsw-alias-border-l2);align-items:center;gap:8px;padding:16px 0;display:flex}.btRowText{flex-direction:column;flex:1;gap:4px;min-width:0;padding-right:48px;display:flex}.btTitle{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:22px}.btDesc{color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:400;line-height:18px}.btSelector{background:var(--dsw-alias-bg-module-platform);height:36px;font:inherit;color:var(--dsw-alias-label-primary);cursor:pointer;border:none;border-radius:18px;align-items:center;gap:12px;padding:0 14px;font-size:14px;line-height:22px;display:inline-flex}.btSelector:hover{background:var(--dsw-alias-interactive-bg-hover)}.btChevron{flex:none}";
|
|
39
39
|
if (typeof document !== "undefined" && document.querySelector('style[data-plugin-css="bash-terminal-row"]') === null) {
|
|
@@ -59,7 +59,7 @@ var en = {
|
|
|
59
59
|
"shell.msys2": "MSYS2",
|
|
60
60
|
"shell.wsl": "WSL"
|
|
61
61
|
};
|
|
62
|
-
var inject = ["slots", "locale", "
|
|
62
|
+
var inject = ["slots", "locale", "configForms"];
|
|
63
63
|
function ShellPreferenceRow({ t, useStore, setShell }) {
|
|
64
64
|
const shell = useStore((s) => s.shell);
|
|
65
65
|
const writable = useStore((s) => s.writable);
|
|
@@ -105,7 +105,7 @@ function ShellPreferenceRow({ t, useStore, setShell }) {
|
|
|
105
105
|
}
|
|
106
106
|
function apply(ctx) {
|
|
107
107
|
ctx.effect(() => ctx.locale.register(SETTINGS_NS, { zh, en }), "bash-terminal: settings dictionaries");
|
|
108
|
-
const scope = ctx.
|
|
108
|
+
const scope = ctx.configForms.get(ENTRY_ID);
|
|
109
109
|
const store = (0, import_dsh_client_store.defineStore)({
|
|
110
110
|
init: () => ({ shell: "powershell", revision: -1, writable: false }),
|
|
111
111
|
actions: {
|
package/lib/dsh-types.d.ts
CHANGED
|
@@ -250,7 +250,6 @@ export interface BashTerminalContext {
|
|
|
250
250
|
tools: ToolsSeam;
|
|
251
251
|
on(event: "system-prompt/assemble", handler: AssembleHandler): void;
|
|
252
252
|
shellEnv: ShellEnvSeam;
|
|
253
|
-
settings: SettingsSeam;
|
|
254
253
|
sandboxPolicy: SandboxPolicySeam;
|
|
255
254
|
sandbox: SandboxSeam;
|
|
256
255
|
get(key: string): unknown;
|
package/lib/dsh.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ export declare const escalationHintMarker: (subject: string) => string;
|
|
|
20
20
|
/** Runtime config schema factory (schemastery fork). */
|
|
21
21
|
export interface SchemasterySchema {
|
|
22
22
|
default(value: unknown): SchemasterySchema;
|
|
23
|
+
/** 0.1.7+: expose the field on the auto-generated settings form. */
|
|
24
|
+
volatile(): SchemasterySchema;
|
|
23
25
|
}
|
|
24
26
|
export interface Schemastery {
|
|
25
27
|
object(fields: Record<string, SchemasterySchema>): SchemasterySchema;
|
package/lib/index.d.ts
CHANGED
|
@@ -7,8 +7,6 @@ export declare const inject: string[];
|
|
|
7
7
|
export declare const SHELLS: readonly ["powershell", "gitbash", "msys2", "wsl"];
|
|
8
8
|
/** The backend used when the caller does not name one. */
|
|
9
9
|
export declare const DEFAULT_SHELL: ShellId;
|
|
10
|
-
/** Settings namespace backing the user-chosen default terminal. */
|
|
11
|
-
export declare const SETTINGS_NAMESPACE = "bash-terminal";
|
|
12
10
|
/** Static shape of the runtime configuration schema. */
|
|
13
11
|
export interface ConfigValues {
|
|
14
12
|
defaultShell: string;
|
package/lib/index.js
CHANGED
|
@@ -33,8 +33,6 @@ export const inject = ["tools", "systemPrompt", "shellEnv", "subprocess", "setti
|
|
|
33
33
|
export const SHELLS = ["powershell", "gitbash", "msys2", "wsl"];
|
|
34
34
|
/** The backend used when the caller does not name one. */
|
|
35
35
|
export const DEFAULT_SHELL = "powershell";
|
|
36
|
-
/** Settings namespace backing the user-chosen default terminal. */
|
|
37
|
-
export const SETTINGS_NAMESPACE = "bash-terminal";
|
|
38
36
|
/** Default per-command timeout (ms). */
|
|
39
37
|
const DEFAULT_TIMEOUT_MS = 120000;
|
|
40
38
|
/** Upper bound a caller's timeoutMs is capped to. */
|
|
@@ -56,7 +54,7 @@ const ENV_OVERRIDES = {
|
|
|
56
54
|
};
|
|
57
55
|
/** Runtime configuration schema. */
|
|
58
56
|
export const Config = z.object({
|
|
59
|
-
defaultShell: z.string().default(DEFAULT_SHELL),
|
|
57
|
+
defaultShell: z.string().default(DEFAULT_SHELL).volatile(),
|
|
60
58
|
timeoutMs: z.number().default(DEFAULT_TIMEOUT_MS),
|
|
61
59
|
maxTimeoutMs: z.number().default(MAX_TIMEOUT_MS),
|
|
62
60
|
pwshPath: z.string().default(""),
|
|
@@ -500,11 +498,19 @@ export function apply(ctx, config = {}) {
|
|
|
500
498
|
}
|
|
501
499
|
const backgroundEnabled = true;
|
|
502
500
|
const paths = resolveAllPaths(config);
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
501
|
+
// 0.1.7: `defaultShell` is a `.volatile()` config field — the loader hands
|
|
502
|
+
// `apply` a live ref and the latest value is resolved on every read; there
|
|
503
|
+
// is no settings registration call anymore (removed with the 0.1.7 host).
|
|
504
|
+
const currentShell = () => {
|
|
505
|
+
const v = config.defaultShell;
|
|
506
|
+
if (typeof v === "string")
|
|
507
|
+
return v;
|
|
508
|
+
const got = typeof v?.get === "function" ? v.get() : undefined;
|
|
509
|
+
return typeof got === "string" ? got : DEFAULT_SHELL;
|
|
510
|
+
};
|
|
511
|
+
if (!SHELLS.includes(currentShell())) {
|
|
512
|
+
throw new Error(`dsh-bash-terminal: invalid defaultShell ${JSON.stringify(currentShell())}`);
|
|
506
513
|
}
|
|
507
|
-
const settingsScope = ctx.settings.register(SETTINGS_NAMESPACE, z.object({ defaultShell: z.union(SHELLS.map((s) => z.const(s))).default(defaultShell) }), { base: { defaultShell } });
|
|
508
514
|
/** Official sandbox-escalation surface (mirrors tool-bash): advertise the
|
|
509
515
|
* escalation modes whenever the deployment confines. */
|
|
510
516
|
const escalationModes = ESCALATION_TARGETS;
|
|
@@ -529,7 +535,7 @@ export function apply(ctx, config = {}) {
|
|
|
529
535
|
});
|
|
530
536
|
const toolName = "shell";
|
|
531
537
|
const terminalRegistry = createTerminalRegistry(ctx);
|
|
532
|
-
ctx.tools.register(terminalTool(ctx, terminalRegistry, paths, () =>
|
|
538
|
+
ctx.tools.register(terminalTool(ctx, terminalRegistry, paths, () => currentShell()));
|
|
533
539
|
// The model-facing description must track the user's chosen default
|
|
534
540
|
// terminal: a static description listing every backend leaves the model
|
|
535
541
|
// guessing which syntax applies. Re-render it on every prompt assembly —
|
|
@@ -537,7 +543,7 @@ export function apply(ctx, config = {}) {
|
|
|
537
543
|
// without a restart.
|
|
538
544
|
ctx.on("system-prompt/assemble", async (_assembly, _context, next) => {
|
|
539
545
|
const assembled = await next();
|
|
540
|
-
const description = toolDescription(backgroundEnabled,
|
|
546
|
+
const description = toolDescription(backgroundEnabled, currentShell());
|
|
541
547
|
const tools = Array.isArray(assembled.tools)
|
|
542
548
|
? assembled.tools.map((tool) => (tool.name === toolName ? { ...tool, description } : tool))
|
|
543
549
|
: assembled.tools;
|
|
@@ -545,7 +551,7 @@ export function apply(ctx, config = {}) {
|
|
|
545
551
|
});
|
|
546
552
|
ctx.tools.register(defineTool({
|
|
547
553
|
name: toolName,
|
|
548
|
-
description: toolDescription(backgroundEnabled,
|
|
554
|
+
description: toolDescription(backgroundEnabled, currentShell()),
|
|
549
555
|
parameters: {
|
|
550
556
|
command: {
|
|
551
557
|
type: "string",
|
|
@@ -644,7 +650,7 @@ export function apply(ctx, config = {}) {
|
|
|
644
650
|
},
|
|
645
651
|
async execute(args, exec) {
|
|
646
652
|
const v = validateArgs(args);
|
|
647
|
-
const shell =
|
|
653
|
+
const shell = currentShell();
|
|
648
654
|
const argv0 = buildArgv(shell, v.command, paths, v.distro);
|
|
649
655
|
if (argv0[0] === undefined) {
|
|
650
656
|
throw new Error(`dsh-bash-terminal: ${shell} backend unavailable - executable not found. Install it or set the corresponding *Path config.`);
|
package/package.json
CHANGED
|
@@ -1,116 +1,115 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "dsh-bash-terminal-ts",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"publishConfig": {
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
".": "./
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"tsconfig.json",
|
|
49
|
-
"tsconfig.
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
"build": "
|
|
59
|
-
"
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
|
|
70
|
-
"@deepseek-ai/dsh-client-
|
|
71
|
-
"@deepseek-ai/dsh-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
"@deepseek-ai/
|
|
78
|
-
"@deepseek-ai/dsh-
|
|
79
|
-
"@deepseek-ai/dsh-
|
|
80
|
-
"@deepseek-ai/dsh-
|
|
81
|
-
"@deepseek-ai/dsh-
|
|
82
|
-
"@deepseek-ai/dsh-
|
|
83
|
-
"@deepseek-ai/dsh-
|
|
84
|
-
"@deepseek-ai/dsh-client-
|
|
85
|
-
"@deepseek-ai/dsh-client-
|
|
86
|
-
"@deepseek-ai/dsh-client-ui-
|
|
87
|
-
"@deepseek-ai/dsh-client-ui-
|
|
88
|
-
"
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
"
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
"@deepseek-ai/
|
|
97
|
-
"@deepseek-ai/dsh-client-
|
|
98
|
-
"@deepseek-ai/dsh-client-
|
|
99
|
-
"@deepseek-ai/dsh-client-ui-
|
|
100
|
-
"@deepseek-ai/dsh-client-ui-
|
|
101
|
-
"@deepseek-ai/dsh-
|
|
102
|
-
"@deepseek-ai/dsh-
|
|
103
|
-
"@deepseek-ai/dsh-
|
|
104
|
-
"@deepseek-ai/dsh-
|
|
105
|
-
"@deepseek-ai/dsh-
|
|
106
|
-
"@deepseek-ai/dsh-
|
|
107
|
-
"@
|
|
108
|
-
"@types/
|
|
109
|
-
"@types/react": "^18.2.0",
|
|
110
|
-
"
|
|
111
|
-
"
|
|
112
|
-
"react": "^18.2.0",
|
|
113
|
-
"
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-bash-terminal-ts",
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"tag": "dsh-0.1.7"
|
|
6
|
+
},
|
|
7
|
+
"description": "DSH plugin: one shell tool that runs commands through PowerShell, Git Bash, MSYS2, or WSL on Windows, with a user-chosen default terminal in the Web UI settings. TypeScript rewrite of MAXeaglet/dsh-bash-terminal with working MSYS2/MINGW64 support.",
|
|
8
|
+
"author": "drscrewdriver",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/drscrewdriver/dsh-bash-terminal-ts.git"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/drscrewdriver/dsh-bash-terminal-ts/issues"
|
|
16
|
+
},
|
|
17
|
+
"homepage": "https://github.com/drscrewdriver/dsh-bash-terminal-ts#readme",
|
|
18
|
+
"keywords": [
|
|
19
|
+
"deepseek-harness",
|
|
20
|
+
"dsh",
|
|
21
|
+
"plugin",
|
|
22
|
+
"terminal",
|
|
23
|
+
"shell",
|
|
24
|
+
"powershell",
|
|
25
|
+
"git-bash",
|
|
26
|
+
"wsl",
|
|
27
|
+
"windows",
|
|
28
|
+
"typescript"
|
|
29
|
+
],
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=22",
|
|
32
|
+
"dsh": ">=0.1.7-rc.1 <0.2.0-0"
|
|
33
|
+
},
|
|
34
|
+
"type": "module",
|
|
35
|
+
"main": "lib/index.js",
|
|
36
|
+
"exports": {
|
|
37
|
+
".": "./lib/index.js",
|
|
38
|
+
"./client": "./lib/client.js",
|
|
39
|
+
"./package.json": "./package.json"
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"lib",
|
|
43
|
+
"dist",
|
|
44
|
+
"src",
|
|
45
|
+
"scripts",
|
|
46
|
+
"template",
|
|
47
|
+
"tsconfig.json",
|
|
48
|
+
"tsconfig.client.json",
|
|
49
|
+
"tsconfig.test.json",
|
|
50
|
+
"README.md",
|
|
51
|
+
"LICENSE",
|
|
52
|
+
"cordis.patch.yml",
|
|
53
|
+
"dsh.plugin.json",
|
|
54
|
+
"screenshots.json"
|
|
55
|
+
],
|
|
56
|
+
"scripts": {
|
|
57
|
+
"build": "tsc -p tsconfig.json && tsc -p tsconfig.client.json && node scripts/build-client.mjs && tsc -p tsconfig.test.json",
|
|
58
|
+
"build:client": "node scripts/build-client.mjs",
|
|
59
|
+
"test": "node test-dist/unit.js && node test-dist/apply.js && node test-dist/client.js && node test-dist/terminal.js"
|
|
60
|
+
},
|
|
61
|
+
"dsh": {
|
|
62
|
+
"bundle": {
|
|
63
|
+
"patch": "./cordis.patch.yml"
|
|
64
|
+
},
|
|
65
|
+
"client": {
|
|
66
|
+
"platform": "web",
|
|
67
|
+
"immediately": true,
|
|
68
|
+
"inject": [
|
|
69
|
+
"@deepseek-ai/dsh-client-locale",
|
|
70
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
71
|
+
"@deepseek-ai/dsh-api-remotes"
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"peerDependencies": {
|
|
76
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
77
|
+
"@deepseek-ai/dsh-tools": ">=0.1.7-rc.1 <0.2.0-0",
|
|
78
|
+
"@deepseek-ai/dsh-settings": ">=0.1.7-rc.1 <0.2.0-0",
|
|
79
|
+
"@deepseek-ai/dsh-llm": ">=0.1.7-rc.1 <0.2.0-0",
|
|
80
|
+
"@deepseek-ai/dsh-shell": ">=0.1.7-rc.1 <0.2.0-0",
|
|
81
|
+
"@deepseek-ai/dsh-sandbox": ">=0.1.7-rc.1 <0.2.0-0",
|
|
82
|
+
"@deepseek-ai/dsh-timeout": ">=0.1.7-rc.1 <0.2.0-0",
|
|
83
|
+
"@deepseek-ai/dsh-client-store": ">=0.1.7-rc.1 <0.2.0-0",
|
|
84
|
+
"@deepseek-ai/dsh-client-locale": ">=0.1.7-rc.1 <0.2.0-0",
|
|
85
|
+
"@deepseek-ai/dsh-client-ui-settings": ">=0.1.7-rc.1 <0.2.0-0",
|
|
86
|
+
"@deepseek-ai/dsh-client-ui-slots": ">=0.1.7-rc.1 <0.2.0-0",
|
|
87
|
+
"@deepseek-ai/dsh-client-ui-primitives": ">=0.1.7-rc.1 <0.2.0-0",
|
|
88
|
+
"react": "^18.2.0"
|
|
89
|
+
},
|
|
90
|
+
"dependencies": {
|
|
91
|
+
"@deepseek-ai/schemastery": "^3.18.4",
|
|
92
|
+
"node-pty": "^1.1.0"
|
|
93
|
+
},
|
|
94
|
+
"devDependencies": {
|
|
95
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
96
|
+
"@deepseek-ai/dsh-client-locale": "0.1.5-rc.2",
|
|
97
|
+
"@deepseek-ai/dsh-client-store": "0.1.5-rc.2",
|
|
98
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.1.5-rc.2",
|
|
99
|
+
"@deepseek-ai/dsh-client-ui-settings": "0.1.5-rc.2",
|
|
100
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.1.5-rc.2",
|
|
101
|
+
"@deepseek-ai/dsh-llm": "0.1.5-rc.2",
|
|
102
|
+
"@deepseek-ai/dsh-sandbox": "0.1.5-rc.2",
|
|
103
|
+
"@deepseek-ai/dsh-settings": "0.1.5-rc.2",
|
|
104
|
+
"@deepseek-ai/dsh-shell": "0.1.5-rc.2",
|
|
105
|
+
"@deepseek-ai/dsh-timeout": "0.1.5-rc.2",
|
|
106
|
+
"@deepseek-ai/dsh-tools": "0.1.5-rc.2",
|
|
107
|
+
"@types/node": "^22.10.0",
|
|
108
|
+
"@types/react": "^18.2.0",
|
|
109
|
+
"@types/react-dom": "^18.2.0",
|
|
110
|
+
"esbuild": "^0.28.2",
|
|
111
|
+
"react": "^18.2.0",
|
|
112
|
+
"react-dom": "^18.2.0",
|
|
113
|
+
"typescript": "^5.9.0"
|
|
114
|
+
}
|
|
115
|
+
}
|
package/screenshots.json
CHANGED
package/src/client.tsx
CHANGED
|
@@ -16,7 +16,8 @@ import { defineStore } from "@deepseek-ai/dsh-client-store";
|
|
|
16
16
|
import { IconChevronDownOutline14, Menu } from "@deepseek-ai/dsh-client-ui-primitives";
|
|
17
17
|
|
|
18
18
|
const SETTINGS_NS = "settings.bash-terminal";
|
|
19
|
-
|
|
19
|
+
/** Entry id of this plugin in the active profile (cordis.patch.yml). */
|
|
20
|
+
const ENTRY_ID = "tool-bash-terminal";
|
|
20
21
|
const SHELLS = ["powershell", "gitbash", "msys2", "wsl"] as const;
|
|
21
22
|
|
|
22
23
|
type ShellOption = (typeof SHELLS)[number];
|
|
@@ -55,7 +56,7 @@ const en: Record<string, string> = {
|
|
|
55
56
|
"shell.wsl": "WSL"
|
|
56
57
|
};
|
|
57
58
|
|
|
58
|
-
export const inject = ["slots", "locale", "
|
|
59
|
+
export const inject = ["slots", "locale", "configForms"];
|
|
59
60
|
|
|
60
61
|
/** Row state snapshot served through the settings store. */
|
|
61
62
|
interface RowState {
|
|
@@ -112,23 +113,23 @@ function ShellPreferenceRow({ t, useStore, setShell }: ShellPreferenceRowProps)
|
|
|
112
113
|
}
|
|
113
114
|
|
|
114
115
|
interface SettingsSnapshot {
|
|
115
|
-
status:
|
|
116
|
+
status: "loading" | "ready" | "unavailable";
|
|
116
117
|
value?: { defaultShell?: string };
|
|
117
|
-
revision: number;
|
|
118
|
+
revision: number | undefined;
|
|
118
119
|
writable: boolean;
|
|
119
120
|
}
|
|
120
121
|
|
|
121
|
-
interface
|
|
122
|
+
interface ConfigFormBinding {
|
|
122
123
|
getSnapshot(): SettingsSnapshot;
|
|
123
124
|
subscribe(fn: () => void): () => void;
|
|
124
|
-
set(field: string, value: string):
|
|
125
|
-
unset():
|
|
125
|
+
set(field: string, value: string): Promise<boolean>;
|
|
126
|
+
unset(field: string): Promise<boolean>;
|
|
126
127
|
}
|
|
127
128
|
|
|
128
129
|
interface ClientContext {
|
|
129
130
|
effect(fn: () => unknown, name?: string): unknown;
|
|
130
131
|
locale: { register(ns: string, dicts: Record<string, Record<string, string>>): unknown };
|
|
131
|
-
|
|
132
|
+
configForms: { get(entryId: string): ConfigFormBinding };
|
|
132
133
|
slots: {
|
|
133
134
|
inject(slot: string, fn: () => unknown, name?: string): void;
|
|
134
135
|
register(options: Record<string, unknown>, Component: unknown): Record<string, unknown>;
|
|
@@ -145,7 +146,7 @@ interface RowStore {
|
|
|
145
146
|
|
|
146
147
|
export function apply(ctx: ClientContext): void {
|
|
147
148
|
ctx.effect(() => ctx.locale.register(SETTINGS_NS, { zh, en }), "bash-terminal: settings dictionaries");
|
|
148
|
-
const scope = ctx.
|
|
149
|
+
const scope = ctx.configForms.get(ENTRY_ID);
|
|
149
150
|
const store = defineStore<RowState, { sync: (draft: RowState, shell?: string, revision?: number, writable?: boolean) => void }>({
|
|
150
151
|
init: (): RowState => ({ shell: "powershell", revision: -1, writable: false }),
|
|
151
152
|
actions: {
|
package/src/dsh-types.ts
CHANGED
|
@@ -303,7 +303,6 @@ export interface BashTerminalContext {
|
|
|
303
303
|
tools: ToolsSeam;
|
|
304
304
|
on(event: "system-prompt/assemble", handler: AssembleHandler): void;
|
|
305
305
|
shellEnv: ShellEnvSeam;
|
|
306
|
-
settings: SettingsSeam;
|
|
307
306
|
sandboxPolicy: SandboxPolicySeam;
|
|
308
307
|
sandbox: SandboxSeam;
|
|
309
308
|
get(key: string): unknown;
|
package/src/dsh.ts
CHANGED
|
@@ -52,6 +52,8 @@ export const escalationHintMarker = sandboxNs.escalationHintMarker as unknown as
|
|
|
52
52
|
/** Runtime config schema factory (schemastery fork). */
|
|
53
53
|
export interface SchemasterySchema {
|
|
54
54
|
default(value: unknown): SchemasterySchema;
|
|
55
|
+
/** 0.1.7+: expose the field on the auto-generated settings form. */
|
|
56
|
+
volatile(): SchemasterySchema;
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
export interface Schemastery {
|
package/src/index.ts
CHANGED
|
@@ -61,8 +61,6 @@ export const inject = ["tools", "systemPrompt", "shellEnv", "subprocess", "setti
|
|
|
61
61
|
export const SHELLS = ["powershell", "gitbash", "msys2", "wsl"] as const;
|
|
62
62
|
/** The backend used when the caller does not name one. */
|
|
63
63
|
export const DEFAULT_SHELL: ShellId = "powershell";
|
|
64
|
-
/** Settings namespace backing the user-chosen default terminal. */
|
|
65
|
-
export const SETTINGS_NAMESPACE = "bash-terminal";
|
|
66
64
|
/** Default per-command timeout (ms). */
|
|
67
65
|
const DEFAULT_TIMEOUT_MS = 120000;
|
|
68
66
|
/** Upper bound a caller's timeoutMs is capped to. */
|
|
@@ -84,6 +82,11 @@ const ENV_OVERRIDES: Record<string, string> = {
|
|
|
84
82
|
GIT_PAGER: "cat"
|
|
85
83
|
};
|
|
86
84
|
|
|
85
|
+
/** Live reference the 0.1.7 loader hands `apply` for `.volatile()` config fields. */
|
|
86
|
+
interface VolatileRef<T> {
|
|
87
|
+
get(): T;
|
|
88
|
+
}
|
|
89
|
+
|
|
87
90
|
/** Static shape of the runtime configuration schema. */
|
|
88
91
|
export interface ConfigValues {
|
|
89
92
|
defaultShell: string;
|
|
@@ -97,7 +100,7 @@ export interface ConfigValues {
|
|
|
97
100
|
|
|
98
101
|
/** Runtime configuration schema. */
|
|
99
102
|
export const Config = z.object({
|
|
100
|
-
defaultShell: z.string().default(DEFAULT_SHELL),
|
|
103
|
+
defaultShell: z.string().default(DEFAULT_SHELL).volatile(),
|
|
101
104
|
timeoutMs: z.number().default(DEFAULT_TIMEOUT_MS),
|
|
102
105
|
maxTimeoutMs: z.number().default(MAX_TIMEOUT_MS),
|
|
103
106
|
pwshPath: z.string().default(""),
|
|
@@ -671,15 +674,18 @@ export function apply(ctx: BashTerminalContext, config: Partial<ConfigValues> =
|
|
|
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;
|
|
@@ -711,7 +717,7 @@ export function apply(ctx: BashTerminalContext, config: Partial<ConfigValues> =
|
|
|
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,7 +832,7 @@ 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
838
|
throw new Error(`dsh-bash-terminal: ${shell} backend unavailable - executable not found. Install it or set the corresponding *Path config.`);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# 终端适配模式模板
|
|
2
|
+
|
|
3
|
+
把 `dsh-bash-terminal-ts` 里那套「一个 `shell` 工具、多种本地终端」的做法抽出来,供你直接复用。
|
|
4
|
+
|
|
5
|
+
- [`terminal-adapter.ts`](./terminal-adapter.ts) — 四种模式已填好的适配器模块,零依赖(只用 `node:fs` / `node:path`),整份复制或只挑一种都行。
|
|
6
|
+
- 本文件 — 每种模式的要点、踩过的坑,以及怎么加第五种。
|
|
7
|
+
|
|
8
|
+
> **本目录已做过分发前清理**:不含作者本机路径、主机名、凭据。示例路径统一写成 `D:\workspace\your-plugin` 这类占位形式。
|
|
9
|
+
> 拷进你自己仓库前,请照做一遍。
|
|
10
|
+
|
|
11
|
+
## 四种模式
|
|
12
|
+
|
|
13
|
+
| 模式 | 实际执行 | 语法 / 路径 | 环境变量 | 可沙箱封闭 |
|
|
14
|
+
|------|----------|-------------|----------|------------|
|
|
15
|
+
| `powershell`(默认) | `pwsh -NoLogo -NoProfile -NonInteractive -Command <cmd>` | PowerShell;`C:\...` | `$env:NAME` | ✅ |
|
|
16
|
+
| `gitbash` | Git for Windows `bash -lc <cmd>` | POSIX;`/d/...`;PATH 含 `/usr/bin`、`/mingw64/bin` | `$NAME` | ❌ |
|
|
17
|
+
| `msys2` | MSYS2 `bash -lc <cmd>`(`C:\msys64\usr\bin\bash.exe`) | POSIX;`/c/...`;PATH 含 `/usr/bin`、`/mingw64/bin`(gcc / make) | `$NAME`(自动注入 `MSYSTEM=MINGW64`) | ❌ |
|
|
18
|
+
| `wsl` | `wsl [-d <distro>] -e bash -lc <cmd>` | Linux;`/mnt/d/...` | `$NAME`(经 `WSLENV`) | ❌(隔离即沙箱) |
|
|
19
|
+
|
|
20
|
+
## 每种模式的要点
|
|
21
|
+
|
|
22
|
+
### 1. Windows 标准模式(PowerShell)
|
|
23
|
+
|
|
24
|
+
- **探测顺序**:`%ProgramFiles%\PowerShell\7\pwsh.exe` → PATH 里各目录的 `pwsh.exe` → `%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe`。PowerShell 7 优先,5.1 只作兜底。
|
|
25
|
+
- **必须带 `-NonInteractive`**:否则命令里任何一处隐含提示都会把采集挂死。工具收到的是一条完整脚本,不是 REPL 会话。
|
|
26
|
+
- **唯一可被沙箱封闭的模式**。走你平台的沙箱门面包装 spawn argv;封闭模式拿不到可用后端时要 **fail-closed 抛错**,不要降级成不封闭。
|
|
27
|
+
|
|
28
|
+
### 2. Git Bash
|
|
29
|
+
|
|
30
|
+
- **`-lc` 不是 `-c`**:`-l` 会 source `/etc/profile`,`/usr/bin`、`/mingw64/bin` 才进 PATH。用裸 `-c`,`git`、`ssh`、`tr`、`sed` 全是 command not found。
|
|
31
|
+
- **必须排除 System32 的 `bash.exe`** —— 这是这套适配里最容易踩的坑。装了 WSL 的机器上 `C:\Windows\System32\bash.exe` 也存在,但它是 **WSL 转发器**,不是 Git Bash。一旦选中,Git Bash 模式的命令会被静默送进 Linux VM,`/d/...` 路径和 Git for Windows 的工具链全都不存在,而报错信息完全指不到根因。
|
|
32
|
+
- **不可沙箱封闭**:DSH 的 Windows ACL restricted-token runner 起不了 Cygwin/MSYS2 进程,bash 启动阶段就 `CreateFileMapping ... Win32 error 5` 中止。硬套沙箱 = 每条命令都失败。
|
|
33
|
+
- 安装位置:`%ProgramFiles%\Git\bin\bash.exe`、`%ProgramFiles%\Git\usr\bin\bash.exe`、`%LOCALAPPDATA%\Programs\Git\bin\bash.exe`(按用户安装)。
|
|
34
|
+
|
|
35
|
+
### 3. MSYS2
|
|
36
|
+
|
|
37
|
+
- **`-lc` 同 Git Bash**,理由一致。
|
|
38
|
+
- **`MSYSTEM=MINGW64` 要自动补上**:`/etc/profile` 靠它把 `/mingw64/bin`(gcc / make)挂进 PATH。不设的话 MSYS2 落在裸 MSYS 环境,工具链整个看不见。调用方显式传了值就尊重调用方。
|
|
39
|
+
- **`msys2.exe` 不能当后端**:它是分配控制台的 Cygwin 启动器,用管道 stdio spawn 出来会**退出码 0、stdout/stderr 都是零字节**——每条命令都"成功且无输出"。模板里把它留在候选表最后只作兜底,正常 `bash.exe` 永远优先。
|
|
40
|
+
- **不可沙箱封闭**,原因同 Git Bash(同一套 Cygwin/MSYS2 运行时)。
|
|
41
|
+
|
|
42
|
+
### 4. WSL
|
|
43
|
+
|
|
44
|
+
- **`-e bash -lc <cmd>`**:`-e` 直接执行、不经额外的 shell 包装,引号能原样穿过边界。指定发行版时插 `-d <distro>`。
|
|
45
|
+
- **`WSLENV` 是白名单,只能叠加、不能重建**:只有列在 `WSLENV` 里的变量才进得了 WSL。宿主本来可能已经有值(Windows Terminal 会导出 `WT_SESSION:WT_PROFILE_ID:`),重建会把这些静默丢掉,用户的终端会话变量就断了。
|
|
46
|
+
- **叠加时要按 `:` 切分再拼**,不能字符串直接拼——宿主值结尾带一个 `:`,直接拼会多出一个空条目。同时要把 `WSLENV` 这个 key 自己排除,否则会再加一条无意义条目。
|
|
47
|
+
- **`wsl.exe` 不用探测**:它是 Windows 组件,固定位置 `%SystemRoot%\System32\wsl.exe`,视为恒存在。
|
|
48
|
+
- **它的隔离本身就是沙箱**,再包一层是多余的;如实上报 `wsl-isolation`。
|
|
49
|
+
|
|
50
|
+
## 加第五种模式
|
|
51
|
+
|
|
52
|
+
以加一个 `cmd.exe` 为例:
|
|
53
|
+
|
|
54
|
+
1. 在 `SHELLS` 里加 id,`ShellId` 会自动带上它(`as const` 联合)。
|
|
55
|
+
2. 在 `ADAPTERS` 里补一条:`label`、`summary`、`toolDescription`、`candidates()`、`configKey`、`confinable`。
|
|
56
|
+
3. 在 `buildArgv` 里加一个 `case`。**保留 `default` 分支的 `never` 穷尽检查**——漏了分支会在编译期报错,而不是运行时掉进某个诡异的默认行为。
|
|
57
|
+
4. 在 `buildEnv` 里加该模式需要的环境修正(有就加,没有就跳过)。
|
|
58
|
+
5. 在 `confinementFor` / `shouldConfine` 里表个态:能不能封闭,不能的话上报哪个 `*-unconfined` 标签。
|
|
59
|
+
6. 配置 schema 里补对应的 `*Path` 覆盖项(可为空串 = 自动探测)。
|
|
60
|
+
7. 设置面板下拉里加一项,**顺序与 `SHELLS` 保持一致**。
|
|
61
|
+
8. 补测试:探测顺序、argv 组装、env 修正各至少一条。
|
|
62
|
+
|
|
63
|
+
## 接进 DSH
|
|
64
|
+
|
|
65
|
+
模板本身不依赖 DSH,接线时用到这几个接缝:
|
|
66
|
+
|
|
67
|
+
| 接缝 | 用途 |
|
|
68
|
+
|------|------|
|
|
69
|
+
| `ctx.subprocess.spawn` / `spawnTerminal` | 派生进程;进程树终止、SIGTERM→grace→SIGKILL、输出 spill |
|
|
70
|
+
| `ctx.jobs` | 后台任务句柄(`run_in_background` / `job_output` / `job_kill`) |
|
|
71
|
+
| `ctx.sandboxPolicy.resolve` | 每次调用解析当次沙箱策略 |
|
|
72
|
+
| `ctx.sandbox.confine` | 仅 PowerShell 走这里;失败要 fail-closed |
|
|
73
|
+
| `ctx.shellEnv.collect` | 采集要下发的环境变量 |
|
|
74
|
+
| `ctx.settings` + `settings.general.item` 槽 | 「默认终端」设置行 |
|
|
75
|
+
|
|
76
|
+
**设计上的一个刻意选择**:这套实现**不占用 `ctx.shell` 能力接缝**。平台自带的沙箱化 `pwsh` 工具保持原样可用,本工具是一个**额外的**、由用户选择的终端入口,两者并存。
|
|
77
|
+
|
|
78
|
+
**另一条硬规矩**:终端由**用户**在 Web UI 里选,模型不能改。工具不把终端参数暴露给模型——否则「用户设定」形同虚设。
|
|
79
|
+
|
|
80
|
+
## 许可
|
|
81
|
+
|
|
82
|
+
与仓库一致:MIT。
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
// Terminal adapter templates — four Windows terminal backends, ready to copy.
|
|
2
|
+
//
|
|
3
|
+
// Extracted from dsh-bash-terminal-ts `src/index.ts` and sanitised for reuse:
|
|
4
|
+
// no author-local paths, no host-specific values. Copy this file — or just the
|
|
5
|
+
// one adapter you need — into your own DSH plugin, then wire the seams listed
|
|
6
|
+
// under "Wiring into DSH" in ./README.md.
|
|
7
|
+
//
|
|
8
|
+
// powershell pwsh -NoLogo -NoProfile -NonInteractive -Command <cmd>
|
|
9
|
+
// gitbash Git for Windows bash -lc <cmd> POSIX; /d/... paths
|
|
10
|
+
// msys2 MSYS2 bash -lc <cmd> POSIX; /c/... paths, GCC/mingw64
|
|
11
|
+
// wsl wsl [-d <distro>] -e bash -lc <cmd> Linux; /mnt/d/... paths
|
|
12
|
+
//
|
|
13
|
+
// The flag choices here are not guesses: each non-obvious one was measured on
|
|
14
|
+
// Windows and carries its reason inline. Read the comment before changing a
|
|
15
|
+
// flag — several of them look arbitrary and are not.
|
|
16
|
+
//
|
|
17
|
+
// Node >= 22. No runtime dependencies beyond node:fs / node:path.
|
|
18
|
+
|
|
19
|
+
import { lstatSync } from "node:fs";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
|
|
22
|
+
/** The four backends, in the order they appear in the settings dropdown. */
|
|
23
|
+
export const SHELLS = ["powershell", "gitbash", "msys2", "wsl"] as const;
|
|
24
|
+
export type ShellId = (typeof SHELLS)[number];
|
|
25
|
+
|
|
26
|
+
/** Chosen when the user has never touched the setting. */
|
|
27
|
+
export const DEFAULT_SHELL: ShellId = "powershell";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Forced into every child process. A shell tool feeds its output to a model, so
|
|
31
|
+
* pagers and colour codes are pure noise: `PAGER=cat` and `NO_COLOR=1` keep the
|
|
32
|
+
* captured stdout byte-for-byte the command's actual output.
|
|
33
|
+
*/
|
|
34
|
+
export const ENV_OVERRIDES: Record<string, string> = {
|
|
35
|
+
NO_COLOR: "1",
|
|
36
|
+
TERM: "dumb",
|
|
37
|
+
PAGER: "cat",
|
|
38
|
+
GIT_PAGER: "cat"
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// ---- executable resolution -------------------------------------------------
|
|
42
|
+
|
|
43
|
+
function candidateExists(candidate: string): boolean {
|
|
44
|
+
try {
|
|
45
|
+
const stat = lstatSync(candidate);
|
|
46
|
+
return stat.isFile() || stat.isSymbolicLink();
|
|
47
|
+
} catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** First candidate that exists on disk, or undefined when none does. */
|
|
53
|
+
export function resolveFromCandidates(candidates: readonly string[]): string | undefined {
|
|
54
|
+
for (const candidate of candidates) {
|
|
55
|
+
if (candidateExists(candidate)) return candidate;
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Well-known PowerShell install locations, then PATH entries. Newest first. */
|
|
61
|
+
export function candidatePwshPaths(env: NodeJS.ProcessEnv = process.env): string[] {
|
|
62
|
+
const programFiles = env.ProgramFiles ?? "C:\\Program Files";
|
|
63
|
+
const systemRoot = env.SystemRoot ?? "C:\\Windows";
|
|
64
|
+
// PowerShell 7 first: pwsh.exe is the cross-platform one and is what the
|
|
65
|
+
// -NoLogo/-NoProfile/-NonInteractive flag set is written against.
|
|
66
|
+
const candidates = [join(programFiles, "PowerShell", "7", "pwsh.exe")];
|
|
67
|
+
for (const entry of (env.PATH ?? "").split(";")) {
|
|
68
|
+
const trimmed = entry.trim().replace(/^"|"$/g, "");
|
|
69
|
+
if (trimmed.length === 0) continue;
|
|
70
|
+
candidates.push(join(trimmed, "pwsh.exe"));
|
|
71
|
+
}
|
|
72
|
+
// Windows PowerShell 5.1 as the floor — present on every Windows install.
|
|
73
|
+
candidates.push(join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe"));
|
|
74
|
+
return candidates;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Git for Windows locations, then PATH `bash.exe` entries EXCLUDING the
|
|
79
|
+
* System32 launcher.
|
|
80
|
+
*
|
|
81
|
+
* That exclusion is the whole point of this function: `C:\Windows\System32\
|
|
82
|
+
* bash.exe` also exists on machines with WSL and is the **WSL forwarder**, not
|
|
83
|
+
* a Git Bash shell. Picking it up silently sends Git Bash commands into a Linux
|
|
84
|
+
* VM, where `/d/...` paths and the Git for Windows toolchain do not exist.
|
|
85
|
+
*/
|
|
86
|
+
export function candidateGitBashPaths(env: NodeJS.ProcessEnv = process.env): string[] {
|
|
87
|
+
const programFiles = env.ProgramFiles ?? "C:\\Program Files";
|
|
88
|
+
const systemRoot = (env.SystemRoot ?? "C:\\Windows").toLowerCase();
|
|
89
|
+
const localAppData = env.LOCALAPPDATA ?? "";
|
|
90
|
+
const candidates = [
|
|
91
|
+
join(programFiles, "Git", "bin", "bash.exe"),
|
|
92
|
+
// usr\bin\bash.exe is the MSYS-rooted twin; some installs only expose this one.
|
|
93
|
+
join(programFiles, "Git", "usr", "bin", "bash.exe")
|
|
94
|
+
];
|
|
95
|
+
// Per-user installs land here.
|
|
96
|
+
if (localAppData.length > 0) candidates.push(join(localAppData, "Programs", "Git", "bin", "bash.exe"));
|
|
97
|
+
for (const entry of (env.PATH ?? "").split(";")) {
|
|
98
|
+
const trimmed = entry.trim().replace(/^"|"$/g, "");
|
|
99
|
+
if (trimmed.length === 0) continue;
|
|
100
|
+
if (trimmed.toLowerCase().includes(systemRoot)) continue; // ← the WSL forwarder
|
|
101
|
+
candidates.push(join(trimmed, "bash.exe"));
|
|
102
|
+
}
|
|
103
|
+
return candidates;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* MSYS2 locations, in preference order: the real `bash.exe` under usr\bin
|
|
108
|
+
* first, then bin\bash.exe, and `msys2.exe` dead last.
|
|
109
|
+
*
|
|
110
|
+
* `msys2.exe` is NOT a usable backend for piped execution. It is the
|
|
111
|
+
* console-allocating Cygwin launcher: spawned with piped stdio it returns exit
|
|
112
|
+
* 0 with zero bytes on both stdout and stderr, so every command looks like it
|
|
113
|
+
* succeeded and printed nothing. It stays in the list only as a last resort;
|
|
114
|
+
* a working `bash.exe` always wins.
|
|
115
|
+
*/
|
|
116
|
+
export function candidateMsys2Paths(env: NodeJS.ProcessEnv = process.env): string[] {
|
|
117
|
+
const candidates = [
|
|
118
|
+
"C:\\msys64\\usr\\bin\\bash.exe",
|
|
119
|
+
"C:\\msys64\\bin\\bash.exe"
|
|
120
|
+
];
|
|
121
|
+
for (const entry of (env.PATH ?? "").split(";")) {
|
|
122
|
+
const trimmed = entry.trim().replace(/^"|"$/g, "");
|
|
123
|
+
if (trimmed.length === 0) continue;
|
|
124
|
+
const lower = trimmed.toLowerCase();
|
|
125
|
+
if (lower.includes("msys64") || lower.includes("mingw64")) {
|
|
126
|
+
candidates.push(join(trimmed, "bash.exe"));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
candidates.push("C:\\msys64\\msys2.exe"); // last resort — see the note above
|
|
130
|
+
return candidates;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** `wsl.exe` always ships at this fixed location; it is never "not installed". */
|
|
134
|
+
export function defaultWslPath(env: NodeJS.ProcessEnv = process.env): string {
|
|
135
|
+
const systemRoot = env.SystemRoot ?? "C:\\Windows";
|
|
136
|
+
return join(systemRoot, "System32", "wsl.exe");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface ResolvedPaths {
|
|
140
|
+
pwsh?: string;
|
|
141
|
+
gitbash?: string;
|
|
142
|
+
msys2?: string;
|
|
143
|
+
wsl?: string;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Per-backend executable overrides; an empty string means "auto-detect". */
|
|
147
|
+
export interface PathConfig {
|
|
148
|
+
pwshPath?: string;
|
|
149
|
+
gitBashPath?: string;
|
|
150
|
+
msys2Path?: string;
|
|
151
|
+
wslPath?: string;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Resolve all four executables. A backend resolves to `undefined` when it is
|
|
156
|
+
* not installed — surface that as a loud per-call error rather than a silent
|
|
157
|
+
* fallback to another shell, or the user's explicit choice gets ignored.
|
|
158
|
+
*/
|
|
159
|
+
export function resolveAllPaths(config: PathConfig = {}, env: NodeJS.ProcessEnv = process.env): ResolvedPaths {
|
|
160
|
+
const pick = (override: string | undefined, candidates: readonly string[]): string | undefined =>
|
|
161
|
+
override !== undefined && override.trim().length > 0 ? override : resolveFromCandidates(candidates);
|
|
162
|
+
return {
|
|
163
|
+
pwsh: pick(config.pwshPath, candidatePwshPaths(env)),
|
|
164
|
+
gitbash: pick(config.gitBashPath, candidateGitBashPaths(env)),
|
|
165
|
+
msys2: pick(config.msys2Path, candidateMsys2Paths(env)),
|
|
166
|
+
// wsl.exe is not probed: it is a Windows component, so treat it as present.
|
|
167
|
+
wsl: config.wslPath !== undefined && config.wslPath.trim().length > 0 ? config.wslPath : defaultWslPath(env)
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ---- argv construction -----------------------------------------------------
|
|
172
|
+
|
|
173
|
+
/** Prepend the backend-specific command flags. `command` always goes last. */
|
|
174
|
+
export function buildArgv(
|
|
175
|
+
shell: ShellId,
|
|
176
|
+
command: string,
|
|
177
|
+
paths: ResolvedPaths,
|
|
178
|
+
distro?: string
|
|
179
|
+
): Array<string | undefined> {
|
|
180
|
+
switch (shell) {
|
|
181
|
+
case "powershell":
|
|
182
|
+
// -NonInteractive so a stray prompt can never hang the capture; the
|
|
183
|
+
// model's command is one complete script, not a REPL session.
|
|
184
|
+
return [paths.pwsh, "-NoLogo", "-NoProfile", "-NonInteractive", "-Command", command];
|
|
185
|
+
case "gitbash":
|
|
186
|
+
// -l (login shell) matters: it sources /etc/profile, which is what puts
|
|
187
|
+
// /usr/bin and /mingw64/bin on PATH. With a bare -c, `git`, `ssh` and
|
|
188
|
+
// friends are "command not found".
|
|
189
|
+
return [paths.gitbash, "-lc", command];
|
|
190
|
+
case "msys2":
|
|
191
|
+
// Same -l reasoning as gitbash, and it also picks up /etc/profile's
|
|
192
|
+
// MSYSTEM handling that puts gcc/make on PATH.
|
|
193
|
+
return [paths.msys2, "-lc", command];
|
|
194
|
+
case "wsl": {
|
|
195
|
+
const distroArg = distro !== undefined && distro.trim().length > 0 ? ["-d", distro.trim()] : [];
|
|
196
|
+
// `-e bash -lc` — not `wsl bash -lc`: `-e` runs the command directly and
|
|
197
|
+
// without a shell wrapper, so quoting survives intact.
|
|
198
|
+
return [paths.wsl, ...distroArg, "-e", "bash", "-lc", command];
|
|
199
|
+
}
|
|
200
|
+
default: {
|
|
201
|
+
const exhaustive: never = shell;
|
|
202
|
+
throw new Error(`invalid shell: ${JSON.stringify(exhaustive)} (expected one of ${SHELLS.join(", ")})`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ---- environment -----------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Merge the host environment with per-backend fixups.
|
|
211
|
+
*
|
|
212
|
+
* @param inheritedWslenv - the ambient WSLENV to layer onto. Callers that
|
|
213
|
+
* replace the child environment wholesale (a PTY path, say) must pass
|
|
214
|
+
* `process.env.WSLENV` explicitly, because the ambient value is not visible
|
|
215
|
+
* through `dshEnv`.
|
|
216
|
+
*/
|
|
217
|
+
export function buildEnv(
|
|
218
|
+
shell: ShellId,
|
|
219
|
+
hostEnv?: Record<string, string>,
|
|
220
|
+
inheritedWslenv: string | undefined = process.env.WSLENV
|
|
221
|
+
): Record<string, string | undefined> {
|
|
222
|
+
const env: Record<string, string | undefined> = { ...ENV_OVERRIDES, ...hostEnv };
|
|
223
|
+
if (shell === "msys2") {
|
|
224
|
+
// Selects the MINGW64 environment, so /mingw64/bin (gcc, make, ...) joins
|
|
225
|
+
// PATH via /etc/profile. Without it MSYS2 defaults to the bare MSYS
|
|
226
|
+
// environment and the toolchain is invisible. An explicit caller value wins.
|
|
227
|
+
if (env.MSYSTEM === undefined) env.MSYSTEM = "MINGW64";
|
|
228
|
+
}
|
|
229
|
+
if (shell === "wsl") {
|
|
230
|
+
const keys = Object.keys(hostEnv ?? {});
|
|
231
|
+
if (keys.length > 0) {
|
|
232
|
+
// WSLENV is an allow-list: only the variables named in it cross into WSL.
|
|
233
|
+
// LAYER onto the inherited value, never rebuild it — dropping inherited
|
|
234
|
+
// entries (Windows Terminal exports e.g. `WT_SESSION:WT_PROFILE_ID:`)
|
|
235
|
+
// would silently stop them crossing.
|
|
236
|
+
//
|
|
237
|
+
// The base is SPLIT on ":" rather than concatenated because the host
|
|
238
|
+
// value ends with a trailing ":", so string concatenation would produce a
|
|
239
|
+
// malformed empty entry.
|
|
240
|
+
const declared = Object.prototype.hasOwnProperty.call(hostEnv ?? {}, "WSLENV");
|
|
241
|
+
const base = declared
|
|
242
|
+
? env.WSLENV
|
|
243
|
+
: (typeof inheritedWslenv === "string" && inheritedWslenv.length > 0 ? inheritedWslenv : env.WSLENV);
|
|
244
|
+
const parts = typeof base === "string" ? base.split(":") : [];
|
|
245
|
+
env.WSLENV = [...parts, ...keys.filter((k) => k !== "WSLENV").flatMap((k) => k.split(":"))]
|
|
246
|
+
.map((p) => p.trim())
|
|
247
|
+
.filter((p) => p.length > 0)
|
|
248
|
+
.join(":");
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return env;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ---- sandbox confinement ---------------------------------------------------
|
|
255
|
+
|
|
256
|
+
export type SandboxMode = "read-only" | "workspace-write" | "danger-full-access";
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Whether a backend can be wrapped by your sandbox seam, and what to report
|
|
260
|
+
* when it cannot.
|
|
261
|
+
*
|
|
262
|
+
* Only PowerShell is confinable on Windows. Git Bash and MSYS2 must NOT be
|
|
263
|
+
* wrapped: DSH's Windows ACL restricted-token runner cannot start a
|
|
264
|
+
* Cygwin/MSYS2 process — bash aborts during startup with
|
|
265
|
+
* `CreateFileMapping ... Win32 error 5` — so wrapping them would abort every
|
|
266
|
+
* command instead of confining it. WSL is its own Linux VM; that isolation IS
|
|
267
|
+
* the sandbox, so wrapping it is redundant.
|
|
268
|
+
*
|
|
269
|
+
* Publish this honestly: report `*-unconfined` rather than claiming a
|
|
270
|
+
* confinement that did not happen.
|
|
271
|
+
*/
|
|
272
|
+
export function confinementFor(mode: SandboxMode, shell: ShellId): string {
|
|
273
|
+
if (shell === "wsl") return "wsl-isolation";
|
|
274
|
+
if (shell === "gitbash") return "gitbash-unconfined";
|
|
275
|
+
if (shell === "msys2") return "msys2-unconfined";
|
|
276
|
+
return mode === "danger-full-access" ? "none" : "powershell-confined";
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** True when the spawn argv must be passed through the sandbox facade. */
|
|
280
|
+
export function shouldConfine(mode: SandboxMode, shell: ShellId): boolean {
|
|
281
|
+
return mode !== "danger-full-access" && shell === "powershell";
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ---- the adapter table -----------------------------------------------------
|
|
285
|
+
|
|
286
|
+
export interface TerminalAdapter {
|
|
287
|
+
readonly id: ShellId;
|
|
288
|
+
/** Shown in the settings dropdown. */
|
|
289
|
+
readonly label: string;
|
|
290
|
+
/** One line describing the backend, suitable for a settings-row caption. */
|
|
291
|
+
readonly summary: string;
|
|
292
|
+
/** Model-facing tool description when this backend is active. */
|
|
293
|
+
readonly toolDescription: string;
|
|
294
|
+
/** Candidate executables, best first. */
|
|
295
|
+
candidates(env?: NodeJS.ProcessEnv): string[];
|
|
296
|
+
/** Path key in your config schema, or undefined when there is nothing to override. */
|
|
297
|
+
readonly configKey?: keyof PathConfig;
|
|
298
|
+
/** Whether this backend's spawn argv may be confined. */
|
|
299
|
+
readonly confinable: boolean;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export const ADAPTERS: Record<ShellId, TerminalAdapter> = {
|
|
303
|
+
powershell: {
|
|
304
|
+
id: "powershell",
|
|
305
|
+
label: "PowerShell",
|
|
306
|
+
summary: "PowerShell 7 (pwsh), the Windows default",
|
|
307
|
+
toolDescription:
|
|
308
|
+
"Execute a PowerShell command (pwsh -NoLogo -NoProfile -NonInteractive -Command <command>) and return its stdout/stderr. PowerShell syntax; native Windows paths (C:\\...); environment variables via $env:NAME.",
|
|
309
|
+
candidates: candidatePwshPaths,
|
|
310
|
+
configKey: "pwshPath",
|
|
311
|
+
confinable: true
|
|
312
|
+
},
|
|
313
|
+
gitbash: {
|
|
314
|
+
id: "gitbash",
|
|
315
|
+
label: "Git Bash",
|
|
316
|
+
summary: "Git for Windows bash — POSIX syntax, /d/... paths",
|
|
317
|
+
toolDescription:
|
|
318
|
+
"Execute a bash command (Git for Windows bash -lc <command>) and return its stdout/stderr. POSIX syntax; paths like /d/workspace; PATH includes /usr/bin and /mingw64/bin so git, npm, ssh etc. work; environment variables via $NAME.",
|
|
319
|
+
candidates: candidateGitBashPaths,
|
|
320
|
+
configKey: "gitBashPath",
|
|
321
|
+
confinable: false
|
|
322
|
+
},
|
|
323
|
+
msys2: {
|
|
324
|
+
id: "msys2",
|
|
325
|
+
label: "MSYS2",
|
|
326
|
+
summary: "MSYS2 bash — POSIX syntax, /c/... paths, full GCC/mingw64 toolchain",
|
|
327
|
+
toolDescription:
|
|
328
|
+
"Execute a bash command (MSYS2 bash -lc <command>) and return its stdout/stderr. POSIX syntax; paths like /c/...; PATH includes /usr/bin and /mingw64/bin so git, npm, gcc, make etc. work; environment variables via $NAME. MSYS2 provides a full GCC/mingw64 toolchain.",
|
|
329
|
+
candidates: candidateMsys2Paths,
|
|
330
|
+
configKey: "msys2Path",
|
|
331
|
+
confinable: false
|
|
332
|
+
},
|
|
333
|
+
wsl: {
|
|
334
|
+
id: "wsl",
|
|
335
|
+
label: "WSL",
|
|
336
|
+
summary: "WSL Linux — /mnt/d/... paths, optional distro",
|
|
337
|
+
toolDescription:
|
|
338
|
+
"Execute a bash command in WSL (wsl [-d <distro>] -e bash -lc <command>) and return its stdout/stderr. Linux syntax; paths like /mnt/d/workspace; environment variables via $NAME (crossing the boundary through WSLENV).",
|
|
339
|
+
candidates: (env = process.env) => [defaultWslPath(env)],
|
|
340
|
+
configKey: "wslPath",
|
|
341
|
+
confinable: false
|
|
342
|
+
}
|
|
343
|
+
};
|