@idlercloud/dsh-git-bash 0.0.0-stage → 0.2.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/LICENSE +22 -0
- package/README.en.md +134 -0
- package/README.md +134 -3
- package/cordis.patch.yml +20 -0
- package/lib/guard.d.ts +47 -0
- package/lib/guard.js +100 -0
- package/lib/index.d.ts +37 -0
- package/lib/index.js +105 -0
- package/lib/prompt.d.ts +39 -0
- package/lib/prompt.js +30 -0
- package/lib/resolve.d.ts +57 -0
- package/lib/resolve.js +190 -0
- package/lib/workdir.d.ts +19 -0
- package/lib/workdir.js +35 -0
- package/package.json +93 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fishquito7
|
|
4
|
+
Copyright (c) 2026 idlercloud
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/README.en.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# @idlercloud/dsh-git-bash
|
|
2
|
+
|
|
3
|
+
Make DSH prefer Git Bash on Windows.
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<b>English</b> · <a href="README.md">中文</a>
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
<img alt="platform" src="https://img.shields.io/badge/platform-Windows-0078D6?style=flat-square">
|
|
11
|
+
<img alt="harness" src="https://img.shields.io/badge/DSH-plugin-4B6BFB?style=flat-square">
|
|
12
|
+
<img alt="license" src="https://img.shields.io/badge/license-MIT-3DA639?style=flat-square">
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
This repository is a fork of [`Fishquito7/dsh-gitbash`](https://github.com/Fishquito7/dsh-gitbash) v0.1.3.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Why
|
|
20
|
+
|
|
21
|
+
Existing models' Bash training corpora are generally far larger than those for PowerShell, so their ability with Bash is also far stronger than with PowerShell. PowerShell itself is hardly well designed either, and even top models often end up wrestling with it.
|
|
22
|
+
|
|
23
|
+
**On Windows, DSH hard-wires the shell to PowerShell only.** There is no bash tool in the tool list at all, even though Git Bash is actually available (which is exactly what Claude Code does on Windows).
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## How it works
|
|
28
|
+
|
|
29
|
+
Reuse the official bash tool: mount it into a separate isolation scope, then attach the Git Bash executor. A system prompt requires the model to prefer Bash.
|
|
30
|
+
|
|
31
|
+
The benefit is a simple implementation and stable behavior: timeouts, argument validation, output truncation and spill files, PTC support and other logic all reuse the official bash tool.
|
|
32
|
+
|
|
33
|
+
In addition, the existing pwsh tool is still kept, because cases such as the registry, services, process handles and native Windows paths are obviously better handled with PowerShell.
|
|
34
|
+
|
|
35
|
+
## Limitations
|
|
36
|
+
|
|
37
|
+
Currently verified only with DSH `0.2.2-rc.1`. Both the compatibility declaration and DSH peer dependencies are restricted to that version. Revalidate the plugin before upgrading DSH.
|
|
38
|
+
|
|
39
|
+
**It does not go through a file sandbox, so it runs only under full access.**
|
|
40
|
+
|
|
41
|
+
Git Bash is an MSYS2 process. Under the Windows restricted token it needs fork, pipes and a private temp directory, and behaves unreliably, so this plugin has no usable sandbox backend. The plugin checks the file policy before invoking the tool and refuses non-full-access calls outright.
|
|
42
|
+
|
|
43
|
+
## Non-goals
|
|
44
|
+
|
|
45
|
+
- Providing a separate bash tool: that would mean copying the official code and rewriting a lot of logic such as timeouts and argument validation.
|
|
46
|
+
- Disabling the pwsh tool: there are still many cases where using PowerShell is simpler.
|
|
47
|
+
- Capabilities beyond Bash: this plugin deliberately stays simple and auditable; other features should live in a separate plugin.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Install, upgrade, and uninstall
|
|
52
|
+
|
|
53
|
+
**Requirements**
|
|
54
|
+
|
|
55
|
+
- Windows 10/11.
|
|
56
|
+
- Git for Windows installed (provides `bash.exe`).
|
|
57
|
+
- DSH `0.2.2-rc.1`.
|
|
58
|
+
|
|
59
|
+
Desktop is a special case: normally install through the built-in plugin manager by entering the package name `@idlercloud/dsh-git-bash`. To install a specific version, enter `@idlercloud/dsh-git-bash@0.2.0`.
|
|
60
|
+
|
|
61
|
+
Otherwise, install into the target profile:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
dsh plugin --profile <profile> add @idlercloud/dsh-git-bash
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`<profile>` is the profile name, such as `web`.
|
|
68
|
+
|
|
69
|
+
Upgrade to the latest version:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
dsh plugin --profile <profile> add @idlercloud/dsh-git-bash@latest
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Uninstall:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
dsh plugin --profile <profile> remove @idlercloud/dsh-git-bash
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Configuration
|
|
84
|
+
|
|
85
|
+
The bundle patch already inserts the `git-bash-executor` and `git-bash-tool` rows; write the `config` below only when overriding defaults.
|
|
86
|
+
|
|
87
|
+
```yaml
|
|
88
|
+
- id: git-bash-executor
|
|
89
|
+
name: '@idlercloud/dsh-git-bash'
|
|
90
|
+
config:
|
|
91
|
+
bashPath: 'C:\Program Files\Git\bin\bash.exe'
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
| Item | Meaning |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `bashPath` | Explicitly specifies `bash.exe`; highest priority. Accepts absolute paths, relative paths, and bare names. Relative paths are resolved against the DSH working directory when the plugin mounts; bare names are looked up on `PATH` |
|
|
97
|
+
| `DSH_GIT_BASH` | Same as above, via the environment variable |
|
|
98
|
+
| Other fields | All upstream `dsh-bash-local` fields are accepted: `cwd`, `timeoutMs`, `maxOutputBytes`, etc. |
|
|
99
|
+
|
|
100
|
+
**The plugin looks for an available Git Bash automatically.** Git's install location varies from machine to machine, so the plugin tries, in order:
|
|
101
|
+
|
|
102
|
+
1. `bashPath` in `config`, or the `DSH_GIT_BASH` environment variable
|
|
103
|
+
2. `GIT_INSTALL_ROOT` (set by Scoop's git package)
|
|
104
|
+
3. the install root inferred from each `git.exe` found on PATH
|
|
105
|
+
4. well-known locations: `%ProgramFiles%\Git\bin`, `%ProgramFiles(x86)%`, `%ProgramW6432%`, `%LOCALAPPDATA%\Programs\Git\bin`, `%USERPROFILE%\scoop`
|
|
106
|
+
|
|
107
|
+
Each candidate is checked for existence; the first existing candidate that is not a WSL launcher is used. `%SystemRoot%\System32\bash.exe` is the legacy WSL launcher and `%LOCALAPPDATA%\Microsoft\WindowsApps\bash.exe` is the Store app execution alias for WSL; both are skipped outright. If no candidate fits, the plugin reports an error listing every path it tried.
|
|
108
|
+
|
|
109
|
+
The resolved result is cached. When the cached file no longer exists, or the configuration changes, the plugin resolves again.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Development
|
|
114
|
+
|
|
115
|
+
Sources live in `src/`; `tsc` compiles them to `lib/`, which is a build artifact and is not committed.
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
npm install
|
|
119
|
+
npm run build
|
|
120
|
+
npm test
|
|
121
|
+
npm run test:git-bash
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`npm test` builds and runs unit tests plus a real tool-service integration test. The integration test asserts permission enforcement through the real `ToolsService` and execution pipeline, without Git Bash or a running DSH instance. It explicitly skips non-Windows platforms because the plugin only registers its guard on Windows.
|
|
125
|
+
|
|
126
|
+
`npm run test:git-bash` separately builds and runs `test/harness.mjs`, requiring Windows and an available Git Bash installation. It executes commands through real cordis and dsh-bash-local and asserts the results, returning a nonzero exit code on failure. It is excluded from the default test suite and does not require starting DSH.
|
|
127
|
+
|
|
128
|
+
See [CHANGELOG.md](https://github.com/idlercloud/dsh-git-bash/blob/main/CHANGELOG.md) for changes.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
[MIT](LICENSE)
|
package/README.md
CHANGED
|
@@ -1,3 +1,134 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# @idlercloud/dsh-git-bash
|
|
2
|
+
|
|
3
|
+
让 DSH 在 Windows 上优先使用 Git Bash。
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="README.en.md">English</a> · <b>中文</b>
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
<img alt="platform" src="https://img.shields.io/badge/platform-Windows-0078D6?style=flat-square">
|
|
11
|
+
<img alt="harness" src="https://img.shields.io/badge/DSH-plugin-4B6BFB?style=flat-square">
|
|
12
|
+
<img alt="license" src="https://img.shields.io/badge/license-MIT-3DA639?style=flat-square">
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
本仓库是 [`Fishquito7/dsh-gitbash`](https://github.com/Fishquito7/dsh-gitbash) v0.1.3 的 fork。
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 为什么
|
|
20
|
+
|
|
21
|
+
现有模型的 Bash 训练语料一般远多于 PowerShell,因此其使用 Bash 的能力也远强于使用 PowerShell。PowerShell 本身也算不上良好设计,哪怕是顶尖模型也经常与其搏斗。
|
|
22
|
+
|
|
23
|
+
**DSH 在 Windows 上把 shell 写死了只能用 PowerShell。** 工具列表里根本没有 bash 这个工具,即使实际上可以使用 Git Bash(Claude Code 在 Windows 上就是这么做的)。
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 如何实现
|
|
28
|
+
|
|
29
|
+
复用官方原有的 bash 工具,把它挂进独立的隔离作用域,再接上 Git Bash 执行器。通过系统提示词要求模型优先使用 Bash。
|
|
30
|
+
|
|
31
|
+
好处是实现简单、行为稳定:超时、参数校验、输出截断与落盘、PTC 适配等逻辑都复用官方 bash 工具。
|
|
32
|
+
|
|
33
|
+
另外,原有的 pwsh 工具仍然保留,因为注册表、服务、进程句柄、Windows 原生路径等情况显然用 PowerShell 来完成更合适。
|
|
34
|
+
|
|
35
|
+
## 限制
|
|
36
|
+
|
|
37
|
+
目前仅在 DSH `0.2.2-rc.1` 上验证,兼容性声明与 DSH peer 依赖均限定为该版本。升级 DSH 前需要重新验证本插件。
|
|
38
|
+
|
|
39
|
+
**不经过文件沙箱,因此只在完全权限下运行**。
|
|
40
|
+
|
|
41
|
+
Git Bash 是 MSYS2 进程,在 Windows 受限令牌下需要 fork、管道与私有临时目录,行为不可靠,所以本插件没有可用的沙箱后端。插件会在调用工具前检查文件策略,非完全权限会直接拒绝调用。
|
|
42
|
+
|
|
43
|
+
## 非目标
|
|
44
|
+
|
|
45
|
+
- 提供单独的 bash 工具:那样要抄官方代码,重写超时、参数校验等大量逻辑。
|
|
46
|
+
- 禁用 pwsh 工具:仍有很多场景使用 PowerShell 更简单。
|
|
47
|
+
- 除 Bash 以外的其他能力:本插件刻意保持简单、可审计,其他功能应当另开插件。
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## 安装、升级与卸载
|
|
52
|
+
|
|
53
|
+
**要求**
|
|
54
|
+
|
|
55
|
+
- Windows 10/11。
|
|
56
|
+
- 已安装 Git for Windows(含 `bash.exe`)。
|
|
57
|
+
- DSH `0.2.2-rc.1`。
|
|
58
|
+
|
|
59
|
+
桌面端是特殊的,一般通过内置的插件管理器安装,输入包名 `@idlercloud/dsh-git-bash` 即可安装,如需安装特定版本,可以输入 `@idlercloud/dsh-git-bash@0.2.0`。
|
|
60
|
+
|
|
61
|
+
其他情况下,把插件安装到目标 profile:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
dsh plugin --profile <profile> add @idlercloud/dsh-git-bash
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`<profile>` 是 profile 名称,例如 `web`。
|
|
68
|
+
|
|
69
|
+
升级到最新版本:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
dsh plugin --profile <profile> add @idlercloud/dsh-git-bash@latest
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
卸载:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
dsh plugin --profile <profile> remove @idlercloud/dsh-git-bash
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 配置
|
|
84
|
+
|
|
85
|
+
插件自带的 bundle patch 已插入 `git-bash-executor` 和 `git-bash-tool` 两行;只有需要覆盖默认配置时才写下面的 `config`。
|
|
86
|
+
|
|
87
|
+
```yaml
|
|
88
|
+
- id: git-bash-executor
|
|
89
|
+
name: '@idlercloud/dsh-git-bash'
|
|
90
|
+
config:
|
|
91
|
+
bashPath: 'C:\Program Files\Git\bin\bash.exe'
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
| 项 | 说明 |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `bashPath` | 显式指定 `bash.exe`,优先级最高;支持绝对路径、相对路径及裸名字。相对路径以插件挂载时 DSH 的当前目录为基准,裸名字会在 PATH 中查找 |
|
|
97
|
+
| `DSH_GIT_BASH` | 同上,走环境变量 |
|
|
98
|
+
| 其它字段 | 沿用上游 `dsh-bash-local` 的全部字段:`cwd`、`timeoutMs`、`maxOutputBytes` 等 |
|
|
99
|
+
|
|
100
|
+
**插件会自动寻找环境中可用的 Git Bash。** Git 的安装位置因人而异,插件会尝试按顺序找:
|
|
101
|
+
|
|
102
|
+
1. `config` 里的 `bashPath`,或环境变量 `DSH_GIT_BASH`
|
|
103
|
+
2. `GIT_INSTALL_ROOT`(Scoop 的 git 包会设)
|
|
104
|
+
3. PATH 中 `git.exe` 的每个命中反推安装根
|
|
105
|
+
4. 常见安装位置:`%ProgramFiles%\Git\bin`、`%ProgramFiles(x86)%`、`%ProgramW6432%`、`%LOCALAPPDATA%\Programs\Git\bin`、`%USERPROFILE%\scoop`
|
|
106
|
+
|
|
107
|
+
每个候选项都会检查其路径是否存在,存在且不是 WSL 启动器的第一个候选会被采用。`%SystemRoot%\System32\bash.exe` 是遗留的 WSL 启动器,`%LOCALAPPDATA%\Microsoft\WindowsApps\bash.exe` 是 WSL 的商店应用执行别名,两者都直接跳过。无合适候选项时报错,错误中列出尝试过的每条路径。
|
|
108
|
+
|
|
109
|
+
解析结果会缓存。缓存的文件不存在了,或者配置变了,就会重新解析。
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## 开发
|
|
114
|
+
|
|
115
|
+
源码在 `src/`,`tsc` 编译到 `lib/`;`lib/` 是构建产物,不入库。
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
npm install
|
|
119
|
+
npm run build
|
|
120
|
+
npm test
|
|
121
|
+
npm run test:git-bash
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`npm test` 会先构建,再运行单元测试和真实工具服务集成测试。集成测试通过真实 `ToolsService` 与工具执行流水线断言权限拦截,无需安装 Git Bash 或启动 DSH;由于插件只在 Windows 注册守卫,该用例在非 Windows 平台明确跳过。
|
|
125
|
+
|
|
126
|
+
`npm run test:git-bash` 单独构建并运行 `test/harness.mjs`,需要 Windows 和可用的 Git Bash。它使用真实 cordis 与 dsh-bash-local 执行命令并断言检查结果,失败时返回非零退出码,不属于默认测试集,也不需要启动 DSH。
|
|
127
|
+
|
|
128
|
+
变更记录见 [CHANGELOG.md](https://github.com/idlercloud/dsh-git-bash/blob/main/CHANGELOG.md)。
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## 许可
|
|
133
|
+
|
|
134
|
+
[MIT](LICENSE)
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# @idlercloud/dsh-git-bash bundle patch.
|
|
2
|
+
#
|
|
3
|
+
# 把两行绑进同一个 "shell" 隔离 realm —— 不需要容器行:
|
|
4
|
+
# 使用 isolate: { shell: dsh-git-bash } 映射,只隔离 shell 服务。
|
|
5
|
+
# 两行的 shell 使用相同标签,因此共享同一个 shell 服务隔离 realm:
|
|
6
|
+
# · git-bash-executor 提供该 realm 内的 ctx.shell(Git Bash 执行器);
|
|
7
|
+
# · git-bash-tool 消费该服务,注册出标准的 bash 工具。
|
|
8
|
+
# 宿主的 pwsh 执行器与 tool-pwsh 完全不受影响——只隔离了 shell 这一个名字。
|
|
9
|
+
#
|
|
10
|
+
# 想换 Git Bash 路径:在 config 里写 bashPath,或设置环境变量 DSH_GIT_BASH。
|
|
11
|
+
- insert:
|
|
12
|
+
- id: git-bash-executor
|
|
13
|
+
name: '@idlercloud/dsh-git-bash'
|
|
14
|
+
isolate:
|
|
15
|
+
shell: dsh-git-bash
|
|
16
|
+
|
|
17
|
+
- id: git-bash-tool
|
|
18
|
+
name: '@deepseek-ai/dsh-tool-bash'
|
|
19
|
+
isolate:
|
|
20
|
+
shell: dsh-git-bash
|
package/lib/guard.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-git-bash — 非完全权限拒绝。
|
|
3
|
+
*
|
|
4
|
+
* 本插件的 `bash` 工具运行在 Git Bash(MSYS2)上。MSYS2 进程在 Windows 受限令牌下
|
|
5
|
+
* 需要 fork、创建管道、写入私有临时目录,无法可靠运行,因此没有可用的沙箱后端。
|
|
6
|
+
* 工具只在文件策略解析为 `danger-full-access` 时执行:有会话时读该会话的策略,
|
|
7
|
+
* 无会话时读实例默认模式;`read-only`、`workspace-write`、策略缺失或解析失败时,
|
|
8
|
+
* 调用在 spawn 之前返回拒绝。
|
|
9
|
+
*
|
|
10
|
+
* 拒绝点用 `ctx.tools.guard()`:guard 在每个 `tools/pre-execute` 监听器之后、
|
|
11
|
+
* 工具体执行之前求值,只能拒绝、不能放行。把检查放在 pre-execute 监听器里会被
|
|
12
|
+
* 其他策略或审批插件的 allow 覆盖,guard 不会。
|
|
13
|
+
*
|
|
14
|
+
* 插件入口只在 win32 调用本函数。
|
|
15
|
+
*
|
|
16
|
+
* @module dsh-git-bash/guard
|
|
17
|
+
*/
|
|
18
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
19
|
+
import type { SandboxMode } from '@deepseek-ai/dsh-sandbox';
|
|
20
|
+
export declare const BASH_TOOL_NAME = "bash";
|
|
21
|
+
export declare const FULL_ACCESS_MODE: SandboxMode;
|
|
22
|
+
/** 收集到的调用事实。 */
|
|
23
|
+
export interface BashAccessFacts {
|
|
24
|
+
mode?: SandboxMode;
|
|
25
|
+
hasPolicy?: boolean;
|
|
26
|
+
error?: unknown;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 纯函数,不做 IO。只有精确的 `danger-full-access` 放行;有异常、缺服务、
|
|
30
|
+
* 模式未知都返回拒绝文案。
|
|
31
|
+
*
|
|
32
|
+
* @param facts - 调用事实。
|
|
33
|
+
* @param facts.mode - 解析出来的沙箱模式,未解析时省略。
|
|
34
|
+
* @param facts.hasPolicy - 宿主是否提供 sandboxPolicy 服务,默认 true。
|
|
35
|
+
* @param facts.error - 解析策略时的异常,没有时省略。
|
|
36
|
+
* @returns 拒绝原因;undefined 表示放行。
|
|
37
|
+
*/
|
|
38
|
+
export declare function bashDenialReason({ mode, hasPolicy, error }?: BashAccessFacts): string | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* 注册工具层守卫。
|
|
41
|
+
*
|
|
42
|
+
* 只处理名为 `bash` 的调用,其余调用直接放行,不读取策略。
|
|
43
|
+
*
|
|
44
|
+
* @param ctx - 插件 context,需已注入 tools 服务。
|
|
45
|
+
* @returns 撤销器。
|
|
46
|
+
*/
|
|
47
|
+
export declare function installBashGuard(ctx: Context): () => void;
|
package/lib/guard.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { scopeOf } from '@deepseek-ai/dsh-scope';
|
|
2
|
+
export const BASH_TOOL_NAME = 'bash';
|
|
3
|
+
export const FULL_ACCESS_MODE = 'danger-full-access';
|
|
4
|
+
/** 拒绝文案的前后缀:原因之后给两条出路 */
|
|
5
|
+
const WHY = '`bash` runs in Git for Windows Git Bash, which cannot enforce the DSH file sandbox, ' +
|
|
6
|
+
'so it runs only while the file policy is `danger-full-access`. ';
|
|
7
|
+
const NEXT = ' Use the `pwsh` tool for commands this policy governs, or ask the user to switch the session access mode.';
|
|
8
|
+
/** 把任意抛出物转成一段可读的原因。 */
|
|
9
|
+
function describeError(error) {
|
|
10
|
+
return error instanceof Error ? error.message : String(error);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* 把原因接在 {@link WHY} 之后,拼成一条拒绝文案。
|
|
14
|
+
* @param detail - 本次拒绝的具体原因。
|
|
15
|
+
*/
|
|
16
|
+
function denyReason(detail) {
|
|
17
|
+
return WHY + detail + NEXT;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 纯函数,不做 IO。只有精确的 `danger-full-access` 放行;有异常、缺服务、
|
|
21
|
+
* 模式未知都返回拒绝文案。
|
|
22
|
+
*
|
|
23
|
+
* @param facts - 调用事实。
|
|
24
|
+
* @param facts.mode - 解析出来的沙箱模式,未解析时省略。
|
|
25
|
+
* @param facts.hasPolicy - 宿主是否提供 sandboxPolicy 服务,默认 true。
|
|
26
|
+
* @param facts.error - 解析策略时的异常,没有时省略。
|
|
27
|
+
* @returns 拒绝原因;undefined 表示放行。
|
|
28
|
+
*/
|
|
29
|
+
export function bashDenialReason({ mode, hasPolicy = true, error } = {}) {
|
|
30
|
+
if (error !== undefined) {
|
|
31
|
+
return denyReason(`Resolving the file policy failed (${describeError(error)}).`);
|
|
32
|
+
}
|
|
33
|
+
if (hasPolicy === false) {
|
|
34
|
+
return denyReason('This host exposes no sandbox policy service, so the file policy cannot be verified.');
|
|
35
|
+
}
|
|
36
|
+
if (mode !== FULL_ACCESS_MODE) {
|
|
37
|
+
const actual = mode === undefined ? 'an unknown mode' : `\`${mode}\``;
|
|
38
|
+
return denyReason(`The file policy is ${actual}.`);
|
|
39
|
+
}
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 收集一次调用的事实:sandboxPolicy 服务、解析出的模式。
|
|
44
|
+
*
|
|
45
|
+
* 所有异常都转成 facts,不向工具层抛出:抛出的异常会成为普通工具错误,模型无法
|
|
46
|
+
* 区分这是权限拒绝。
|
|
47
|
+
*
|
|
48
|
+
* @param ctx - 插件 context。
|
|
49
|
+
* @param agent - 发起调用的 agent。
|
|
50
|
+
* @returns `{ hasPolicy, mode?, error? }`。
|
|
51
|
+
*/
|
|
52
|
+
function collectFacts(ctx, agent) {
|
|
53
|
+
const service = ctx.get('sandboxPolicy');
|
|
54
|
+
if (service === undefined)
|
|
55
|
+
return { hasPolicy: false };
|
|
56
|
+
try {
|
|
57
|
+
const { mode } = service.resolve({ session: agent?.session });
|
|
58
|
+
return { hasPolicy: true, mode };
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
return { hasPolicy: true, error };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 这次 `bash` 调用是否属于本插件注册的工具。
|
|
66
|
+
*
|
|
67
|
+
* 工具层允许 agent 作用域的同名定义遮蔽全局工具。其他插件可能注册自己的 `bash`,
|
|
68
|
+
* 策略与本插件不同;只按名字拦截会拒绝这些工具。因此比较调用方本步能执行的定义与
|
|
69
|
+
* 插件注册作用域中的定义:两者都存在且是同一个对象时才由本插件裁决。
|
|
70
|
+
*
|
|
71
|
+
* @param ctx - 插件 context。
|
|
72
|
+
* @param exec - 待执行的工具调用。
|
|
73
|
+
* @returns 该调用是否由本插件裁决。
|
|
74
|
+
*/
|
|
75
|
+
function ownsBashCall(ctx, exec) {
|
|
76
|
+
const visible = ctx.tools.executable(BASH_TOOL_NAME, exec.agent, exec.toolSet);
|
|
77
|
+
const registered = ctx.tools.get(BASH_TOOL_NAME, scopeOf(ctx));
|
|
78
|
+
return visible !== undefined && visible === registered;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* 注册工具层守卫。
|
|
82
|
+
*
|
|
83
|
+
* 只处理名为 `bash` 的调用,其余调用直接放行,不读取策略。
|
|
84
|
+
*
|
|
85
|
+
* @param ctx - 插件 context,需已注入 tools 服务。
|
|
86
|
+
* @returns 撤销器。
|
|
87
|
+
*/
|
|
88
|
+
export function installBashGuard(ctx) {
|
|
89
|
+
return ctx.tools.guard((exec) => {
|
|
90
|
+
if (exec.name !== BASH_TOOL_NAME)
|
|
91
|
+
return undefined;
|
|
92
|
+
if (!ownsBashCall(ctx, exec))
|
|
93
|
+
return undefined;
|
|
94
|
+
const reason = bashDenialReason(collectFacts(ctx, exec.agent));
|
|
95
|
+
if (reason === undefined)
|
|
96
|
+
return undefined;
|
|
97
|
+
ctx.logger.info('dsh-git-bash: denied `bash` before spawn — %s', reason);
|
|
98
|
+
return reason;
|
|
99
|
+
});
|
|
100
|
+
}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-git-bash — 在 Windows 上把 DSH 的原生 bash 工具接到 Git Bash。
|
|
3
|
+
*
|
|
4
|
+
* 设计:不自己写工具,复用上游两个现成件——
|
|
5
|
+
* - @deepseek-ai/dsh-bash-local 的进程机制(超时、输出上限、spill、后台句柄);
|
|
6
|
+
* - @deepseek-ai/dsh-tool-bash 的完整工具 schema、渲染与退出码标记。
|
|
7
|
+
* 本插件只做三件事:
|
|
8
|
+
* - 解析 Git for Windows 里 bash.exe 的绝对路径并调用
|
|
9
|
+
* - 给模型补一段系统提示词,说明这个是 MSYS 的 bash;
|
|
10
|
+
* - 在会话文件策略不是完全权限时,于 spawn 之前拒绝 `bash` 调用。
|
|
11
|
+
*
|
|
12
|
+
* @module dsh-git-bash
|
|
13
|
+
*/
|
|
14
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
15
|
+
import { type Config as BashConfig } from '@deepseek-ai/dsh-bash-local';
|
|
16
|
+
import z from '@deepseek-ai/schemastery';
|
|
17
|
+
import { BashResolver } from './resolve.js';
|
|
18
|
+
/** 加载器读取的插件元数据。 */
|
|
19
|
+
export declare const name = "@idlercloud/dsh-git-bash";
|
|
20
|
+
export declare const inject: string[];
|
|
21
|
+
export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
22
|
+
bashPath: z<string, string, "plain">;
|
|
23
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
24
|
+
bashPath: z<string, string, "plain">;
|
|
25
|
+
}>>, "plain">;
|
|
26
|
+
export { BashResolver };
|
|
27
|
+
/** 插件行的配置:执行器配置之外,本插件多一个 bashPath。 */
|
|
28
|
+
export type PluginConfig = BashConfig & {
|
|
29
|
+
bashPath?: string;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* 挂载执行器与权限闸门。
|
|
33
|
+
*
|
|
34
|
+
* @param ctx - 挂载点 context(位于 isolate 隔离作用域内)。
|
|
35
|
+
* @param config - 组合条目配置,可含 bashPath / cwd / timeoutMs 等。
|
|
36
|
+
*/
|
|
37
|
+
export declare function apply(ctx: Context, config: PluginConfig): void;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-git-bash — 在 Windows 上把 DSH 的原生 bash 工具接到 Git Bash。
|
|
3
|
+
*
|
|
4
|
+
* 设计:不自己写工具,复用上游两个现成件——
|
|
5
|
+
* - @deepseek-ai/dsh-bash-local 的进程机制(超时、输出上限、spill、后台句柄);
|
|
6
|
+
* - @deepseek-ai/dsh-tool-bash 的完整工具 schema、渲染与退出码标记。
|
|
7
|
+
* 本插件只做三件事:
|
|
8
|
+
* - 解析 Git for Windows 里 bash.exe 的绝对路径并调用
|
|
9
|
+
* - 给模型补一段系统提示词,说明这个是 MSYS 的 bash;
|
|
10
|
+
* - 在会话文件策略不是完全权限时,于 spawn 之前拒绝 `bash` 调用。
|
|
11
|
+
*
|
|
12
|
+
* @module dsh-git-bash
|
|
13
|
+
*/
|
|
14
|
+
import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local';
|
|
15
|
+
import z from '@deepseek-ai/schemastery';
|
|
16
|
+
import { bashDenialReason, FULL_ACCESS_MODE, installBashGuard } from './guard.js';
|
|
17
|
+
import { installGitBashPrompt } from './prompt.js';
|
|
18
|
+
import { BashResolver } from './resolve.js';
|
|
19
|
+
import { normalizeWorkdir } from './workdir.js';
|
|
20
|
+
/** 加载器读取的插件元数据。 */
|
|
21
|
+
export const name = '@idlercloud/dsh-git-bash';
|
|
22
|
+
export const inject = ['subprocess', 'tools'];
|
|
23
|
+
export const Config = z.object({
|
|
24
|
+
...LocalBashExecutor.Config.dict,
|
|
25
|
+
bashPath: z.string().description('Git Bash 路径,可使用绝对路径、相对路径或 PATH 中的可执行文件名。'),
|
|
26
|
+
});
|
|
27
|
+
// 暴露给测试:harness 用它按真实环境解析与列出候选。
|
|
28
|
+
export { BashResolver };
|
|
29
|
+
/**
|
|
30
|
+
* 本地 bash 执行器。
|
|
31
|
+
*
|
|
32
|
+
* 覆盖公开入口 `resolve` 与 `execute`:`resolve` 把 `workdir` 归一化成 Windows 路径,
|
|
33
|
+
* `execute` 把 shell 换成解析到的 Git Bash,执行语义仍交给基类的 `executeArgv`。
|
|
34
|
+
* 解析放在第一次 execute 里。
|
|
35
|
+
*/
|
|
36
|
+
class GitBashExecutor extends LocalBashExecutor {
|
|
37
|
+
get sandboxMode() {
|
|
38
|
+
return FULL_ACCESS_MODE;
|
|
39
|
+
}
|
|
40
|
+
assertPolicy(spec) {
|
|
41
|
+
const reason = bashDenialReason({ mode: spec.sandboxPolicy?.mode });
|
|
42
|
+
if (reason) {
|
|
43
|
+
const error = new Error(reason);
|
|
44
|
+
error.code = 'GIT_BASH_POLICY_DENIED';
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
gitBashResolver;
|
|
49
|
+
gitBashPathLogged = false;
|
|
50
|
+
/**
|
|
51
|
+
* @param ctx - 执行器 context。
|
|
52
|
+
* @param config - 插件配置对象。
|
|
53
|
+
* @param resolver - 本次挂载专用的 Git Bash 发现器。
|
|
54
|
+
*/
|
|
55
|
+
constructor(ctx, config, resolver) {
|
|
56
|
+
super(ctx, config);
|
|
57
|
+
// 宿主把服务包成 Proxy,私有字段读不到;普通属性可以穿透。
|
|
58
|
+
this.gitBashResolver = resolver;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* 归一化 `workdir`:`/c/...` 转成 `C:/...`,`/tmp` 这类没有盘符的 MSYS 路径直接报错。
|
|
62
|
+
* @param request - 工具层传入的执行请求。
|
|
63
|
+
* @returns 执行规格,`workdir` 已换成 Windows 路径。
|
|
64
|
+
*/
|
|
65
|
+
resolve(request) {
|
|
66
|
+
this.assertPolicy(request);
|
|
67
|
+
const spec = super.resolve(request);
|
|
68
|
+
return { ...spec, workdir: normalizeWorkdir(spec.workdir) };
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* 把公开入口的 `bash -c <command>` 换成解析到的 Git Bash。
|
|
72
|
+
*
|
|
73
|
+
* 基类的 execute 只做 executeArgv(spec, ['bash', '-c', spec.command]);
|
|
74
|
+
* 这里填入绝对路径,超时、输出上限、后台句柄等仍由 executeArgv 负责。
|
|
75
|
+
* 第一次解析成功时打一条日志,后续调用只读缓存。
|
|
76
|
+
* @param spec - resolve() 产出的执行规格。
|
|
77
|
+
* @returns ShellExecution 句柄。
|
|
78
|
+
*/
|
|
79
|
+
async execute(spec) {
|
|
80
|
+
this.assertPolicy(spec);
|
|
81
|
+
const bashPath = this.gitBashResolver.resolve();
|
|
82
|
+
if (!this.gitBashPathLogged) {
|
|
83
|
+
this.gitBashPathLogged = true;
|
|
84
|
+
this.ctx.logger.info('dsh-git-bash: bash.exe -> %s', bashPath);
|
|
85
|
+
}
|
|
86
|
+
return this.executeArgv(spec, [bashPath, '-c', spec.command]);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* 挂载执行器与权限闸门。
|
|
91
|
+
*
|
|
92
|
+
* @param ctx - 挂载点 context(位于 isolate 隔离作用域内)。
|
|
93
|
+
* @param config - 组合条目配置,可含 bashPath / cwd / timeoutMs 等。
|
|
94
|
+
*/
|
|
95
|
+
export function apply(ctx, config) {
|
|
96
|
+
// bundle 平台检查之外,直接加载插件时也不在其他平台注册执行器。
|
|
97
|
+
if (process.platform !== 'win32')
|
|
98
|
+
return;
|
|
99
|
+
const resolver = new BashResolver({ bashPath: config.bashPath });
|
|
100
|
+
installGitBashPrompt(ctx);
|
|
101
|
+
installBashGuard(ctx);
|
|
102
|
+
ctx.logger.info('dsh-git-bash: `bash` is denied before spawn unless the session file policy is `%s`.', FULL_ACCESS_MODE);
|
|
103
|
+
// 构造即注册:构造函数把执行器挂到隔离作用域的 ctx.shell 上,实例由 context 持有。
|
|
104
|
+
new GitBashExecutor(ctx, config, resolver);
|
|
105
|
+
}
|
package/lib/prompt.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-git-bash — 系统提示词。
|
|
3
|
+
*
|
|
4
|
+
* 官方 bash 工具的提示词默认背后是 POSIX bash,所以只交代"每次调用都是新 shell"
|
|
5
|
+
* 和退出码标记。在 Windows 上这个前提不成立:命令实际跑在 Git for Windows 的
|
|
6
|
+
* Git Bash(MSYS2)里,路径方言、会话持久化语义、以及"哪些活该交给 pwsh"都和模型
|
|
7
|
+
* 对 bash 的默认印象不同。这里把差异补成一段独立 section。
|
|
8
|
+
*
|
|
9
|
+
* 注册成独立 section 而不是改官方文本:官方 `tool:bash` 的文本由
|
|
10
|
+
* @deepseek-ai/dsh-tool-bash 拥有,改它要 fork 整个工具;同一 order 下 section 按
|
|
11
|
+
* 名字的 code-unit 顺序排,"tool:bash" 是 "tool:bash:git-bash" 的前缀,所以这段
|
|
12
|
+
* 稳定落在官方那段之后。
|
|
13
|
+
*
|
|
14
|
+
* 插件入口只在 win32 调用本函数:其他平台上这段提示词只会误导模型。
|
|
15
|
+
*
|
|
16
|
+
* @module dsh-git-bash/prompt
|
|
17
|
+
*/
|
|
18
|
+
import type { Context, Disposable } from '@deepseek-ai/cordis';
|
|
19
|
+
import type { SystemPrompt } from '@deepseek-ai/dsh-system-prompt';
|
|
20
|
+
/** 注册用的 section 名:与官方 `tool:bash` 不同名,二者共存。 */
|
|
21
|
+
export declare const SECTION_NAME = "tool:bash:git-bash";
|
|
22
|
+
/**
|
|
23
|
+
* 模型可见的补充说明。
|
|
24
|
+
*
|
|
25
|
+
* 只写官方工具描述里没有的:bash 跑在哪个 shell、路径方言、不支持文件沙箱、与 pwsh 的分工。
|
|
26
|
+
*/
|
|
27
|
+
export declare const GIT_BASH_PROMPT: string;
|
|
28
|
+
/** 可注入的选项,测试用来替换 prompt 服务。 */
|
|
29
|
+
export interface GitBashPromptOptions {
|
|
30
|
+
prompt?: SystemPrompt;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 注册系统提示词 section。
|
|
34
|
+
*
|
|
35
|
+
* @param ctx - 插件 context,宿主需已提供 systemPrompt。
|
|
36
|
+
* @param options - 测试用:prompt 可注入。
|
|
37
|
+
* @returns effect disposer;不注册时 undefined。
|
|
38
|
+
*/
|
|
39
|
+
export declare function installGitBashPrompt(ctx: Context, options?: GitBashPromptOptions): Disposable<Promise<void>> | undefined;
|
package/lib/prompt.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** 注册用的 section 名:与官方 `tool:bash` 不同名,二者共存。 */
|
|
2
|
+
export const SECTION_NAME = 'tool:bash:git-bash';
|
|
3
|
+
/**
|
|
4
|
+
* 模型可见的补充说明。
|
|
5
|
+
*
|
|
6
|
+
* 只写官方工具描述里没有的:bash 跑在哪个 shell、路径方言、不支持文件沙箱、与 pwsh 的分工。
|
|
7
|
+
*/
|
|
8
|
+
export const GIT_BASH_PROMPT = 'The `bash` tool runs commands in Git for Windows Git Bash, an MSYS2 shell. ' +
|
|
9
|
+
'In this shell `/c/...`, `~`, and `/tmp` are MSYS paths, and an unquoted `C:\\...` drops its backslashes, ' +
|
|
10
|
+
'so write drive paths as `/c/...` or quoted `C:/...`; paths handed to other file tools (such as `read` and ' +
|
|
11
|
+
'`write`) usually stay Windows paths. ' +
|
|
12
|
+
'`workdir` accepts Windows paths (`C:/...`) and MSYS drive paths (`/c/...`, normalized to `C:/...`); ' +
|
|
13
|
+
'drive-less MSYS paths such as `/tmp` are rejected. `workdir` is not shell-expanded, so `~` and `$VAR` ' +
|
|
14
|
+
'are taken literally. ' +
|
|
15
|
+
'This tool does not support a file sandbox, so it is available only under `danger-full-access`. ' +
|
|
16
|
+
'Prefer `bash` for command-line work. Use `pwsh` only when the task is Windows-native, such as cmdlets, ' +
|
|
17
|
+
'the registry, services, and WMI, or when `bash` is unavailable.';
|
|
18
|
+
/**
|
|
19
|
+
* 注册系统提示词 section。
|
|
20
|
+
*
|
|
21
|
+
* @param ctx - 插件 context,宿主需已提供 systemPrompt。
|
|
22
|
+
* @param options - 测试用:prompt 可注入。
|
|
23
|
+
* @returns effect disposer;不注册时 undefined。
|
|
24
|
+
*/
|
|
25
|
+
export function installGitBashPrompt(ctx, options = {}) {
|
|
26
|
+
const prompt = options.prompt ?? ctx.get('systemPrompt');
|
|
27
|
+
if (!prompt)
|
|
28
|
+
return undefined;
|
|
29
|
+
return ctx.effect(() => prompt.section({ name: SECTION_NAME, order: prompt.getSectionOrder('TOOL_BASH'), text: GIT_BASH_PROMPT }), 'dsh-git-bash: system prompt');
|
|
30
|
+
}
|
package/lib/resolve.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Bash 的发现与选择。
|
|
3
|
+
*
|
|
4
|
+
* 顺序:
|
|
5
|
+
* 1. config 里的 bashPath / 环境变量 DSH_GIT_BASH
|
|
6
|
+
* 2. GIT_INSTALL_ROOT
|
|
7
|
+
* 3. PATH 中 git.exe 反推安装根
|
|
8
|
+
* 4. 常见安装位置
|
|
9
|
+
*
|
|
10
|
+
* 每个候选先判断是否存在;`%SystemRoot%\System32\bash.exe` 与
|
|
11
|
+
* `%LOCALAPPDATA%\Microsoft\WindowsApps\bash.exe` 是 WSL 启动器,直接跳过。
|
|
12
|
+
* 显式配置里的裸名字直接从 PATH 解析成绝对路径。
|
|
13
|
+
*
|
|
14
|
+
* 解析结果会缓存。缓存命中时只复核文件是否还在,不再探测。配置变化时由新的解析器接管。
|
|
15
|
+
*
|
|
16
|
+
* @module dsh-git-bash/resolve
|
|
17
|
+
*/
|
|
18
|
+
import { lstatSync, statSync } from 'node:fs';
|
|
19
|
+
/** 构造选项;env、stat、lstat 可在单测里换成假实现。 */
|
|
20
|
+
export interface BashResolverOptions {
|
|
21
|
+
bashPath?: string;
|
|
22
|
+
env?: NodeJS.ProcessEnv;
|
|
23
|
+
stat?: typeof statSync;
|
|
24
|
+
lstat?: typeof lstatSync;
|
|
25
|
+
}
|
|
26
|
+
export declare class BashResolver {
|
|
27
|
+
private readonly env;
|
|
28
|
+
private readonly stat;
|
|
29
|
+
private readonly lstat;
|
|
30
|
+
private readonly configuredPath;
|
|
31
|
+
private cached;
|
|
32
|
+
/** 配置与环境在创建时固定;配置变化时由新的解析器接管。 */
|
|
33
|
+
constructor({ bashPath, env, stat, lstat }?: BashResolverOptions);
|
|
34
|
+
/**
|
|
35
|
+
* 按可靠性排序列出候选 bash.exe,未做存在性检查。
|
|
36
|
+
* @returns 候选路径(已去重)。
|
|
37
|
+
*/
|
|
38
|
+
candidates(): string[];
|
|
39
|
+
/**
|
|
40
|
+
* 解析要执行的 bash.exe。缓存命中时只复核文件是否还在。
|
|
41
|
+
* @returns bash.exe 的绝对路径。
|
|
42
|
+
* @throws 显式配置不合法或找不到 Git Bash 时抛出带完整排查信息的错误(不静默回退到 WSL)。
|
|
43
|
+
*/
|
|
44
|
+
resolve(): string;
|
|
45
|
+
/**
|
|
46
|
+
* 校验显式指定的 bash:路径必须存在且不是 WSL 启动器。
|
|
47
|
+
* 裸名字从 PATH 选择第一个非 WSL 的 bash,返回其绝对路径。
|
|
48
|
+
*/
|
|
49
|
+
private validateExplicit;
|
|
50
|
+
/**
|
|
51
|
+
* 是否为 WSL 的 bash 启动器:`%SystemRoot%\System32\bash.exe`,或当前用户的
|
|
52
|
+
* `%LOCALAPPDATA%\Microsoft\WindowsApps\bash.exe`。
|
|
53
|
+
*/
|
|
54
|
+
private isWslLauncher;
|
|
55
|
+
private exists;
|
|
56
|
+
private findExecutables;
|
|
57
|
+
}
|
package/lib/resolve.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Bash 的发现与选择。
|
|
3
|
+
*
|
|
4
|
+
* 顺序:
|
|
5
|
+
* 1. config 里的 bashPath / 环境变量 DSH_GIT_BASH
|
|
6
|
+
* 2. GIT_INSTALL_ROOT
|
|
7
|
+
* 3. PATH 中 git.exe 反推安装根
|
|
8
|
+
* 4. 常见安装位置
|
|
9
|
+
*
|
|
10
|
+
* 每个候选先判断是否存在;`%SystemRoot%\System32\bash.exe` 与
|
|
11
|
+
* `%LOCALAPPDATA%\Microsoft\WindowsApps\bash.exe` 是 WSL 启动器,直接跳过。
|
|
12
|
+
* 显式配置里的裸名字直接从 PATH 解析成绝对路径。
|
|
13
|
+
*
|
|
14
|
+
* 解析结果会缓存。缓存命中时只复核文件是否还在,不再探测。配置变化时由新的解析器接管。
|
|
15
|
+
*
|
|
16
|
+
* @module dsh-git-bash/resolve
|
|
17
|
+
*/
|
|
18
|
+
import { lstatSync, statSync } from 'node:fs';
|
|
19
|
+
import { win32 } from 'node:path';
|
|
20
|
+
/** 比较路径用的归一化:统一分隔符,忽略大小写。 */
|
|
21
|
+
const canonicalPath = (path) => path.replaceAll('/', '\\').toLowerCase();
|
|
22
|
+
/** 显式配置不合法时的错误。 */
|
|
23
|
+
function invalidExplicit(value, detail) {
|
|
24
|
+
const error = new Error(`dsh-git-bash: bashPath / DSH_GIT_BASH points at ${value}, but ${detail}.`);
|
|
25
|
+
error.code = 'GIT_BASH_NOT_FOUND';
|
|
26
|
+
return error;
|
|
27
|
+
}
|
|
28
|
+
/** 值里是否带路径分隔符:带的是路径,不带的是交给 PATH 的裸名字。 */
|
|
29
|
+
const hasSeparator = (value) => value.includes('\\') || value.includes('/');
|
|
30
|
+
export class BashResolver {
|
|
31
|
+
env;
|
|
32
|
+
stat;
|
|
33
|
+
lstat;
|
|
34
|
+
configuredPath;
|
|
35
|
+
cached;
|
|
36
|
+
/** 配置与环境在创建时固定;配置变化时由新的解析器接管。 */
|
|
37
|
+
constructor({ bashPath, env = process.env, stat = statSync, lstat = lstatSync } = {}) {
|
|
38
|
+
this.env = env;
|
|
39
|
+
const explicit = bashPath || this.env.DSH_GIT_BASH;
|
|
40
|
+
// 在创建时固定路径基准,避免执行命令的 workdir 或后续 cwd 变化影响程序位置。
|
|
41
|
+
this.configuredPath = explicit && hasSeparator(explicit) ? win32.resolve(explicit) : explicit;
|
|
42
|
+
this.stat = stat;
|
|
43
|
+
this.lstat = lstat;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 按可靠性排序列出候选 bash.exe,未做存在性检查。
|
|
47
|
+
* @returns 候选路径(已去重)。
|
|
48
|
+
*/
|
|
49
|
+
candidates() {
|
|
50
|
+
const env = this.env;
|
|
51
|
+
const list = [];
|
|
52
|
+
const add = (candidate) => {
|
|
53
|
+
if (list.some((existing) => existing.toLowerCase() === candidate.toLowerCase()))
|
|
54
|
+
return;
|
|
55
|
+
list.push(candidate);
|
|
56
|
+
};
|
|
57
|
+
const addRoot = (root) => {
|
|
58
|
+
add(win32.join(root, 'bin\\bash.exe'));
|
|
59
|
+
add(win32.join(root, 'usr\\bin\\bash.exe'));
|
|
60
|
+
};
|
|
61
|
+
// 2. GIT_INSTALL_ROOT(Scoop 的 git 包会设)
|
|
62
|
+
if (env.GIT_INSTALL_ROOT)
|
|
63
|
+
addRoot(env.GIT_INSTALL_ROOT);
|
|
64
|
+
// 3. PATH 中 git.exe 的命中反推安装根
|
|
65
|
+
for (const git of this.findExecutables('git')) {
|
|
66
|
+
const dir = win32.dirname(git);
|
|
67
|
+
add(win32.join(dir, '..\\bin\\bash.exe'));
|
|
68
|
+
add(win32.join(dir, '..\\usr\\bin\\bash.exe'));
|
|
69
|
+
}
|
|
70
|
+
// 4. 常见安装位置
|
|
71
|
+
add(win32.join(env.ProgramFiles ?? 'C:\\Program Files', 'Git\\bin\\bash.exe'));
|
|
72
|
+
for (const key of ['ProgramFiles(x86)', 'ProgramW6432']) {
|
|
73
|
+
const value = env[key];
|
|
74
|
+
if (value)
|
|
75
|
+
add(win32.join(value, 'Git\\bin\\bash.exe'));
|
|
76
|
+
}
|
|
77
|
+
if (env.LOCALAPPDATA)
|
|
78
|
+
add(win32.join(env.LOCALAPPDATA, 'Programs\\Git\\bin\\bash.exe'));
|
|
79
|
+
if (env.USERPROFILE)
|
|
80
|
+
add(win32.join(env.USERPROFILE, 'scoop\\apps\\git\\current\\bin\\bash.exe'));
|
|
81
|
+
return list;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* 解析要执行的 bash.exe。缓存命中时只复核文件是否还在。
|
|
85
|
+
* @returns bash.exe 的绝对路径。
|
|
86
|
+
* @throws 显式配置不合法或找不到 Git Bash 时抛出带完整排查信息的错误(不静默回退到 WSL)。
|
|
87
|
+
*/
|
|
88
|
+
resolve() {
|
|
89
|
+
// 缓存优先:上次解析成功的路径还在就直接用。
|
|
90
|
+
let stale;
|
|
91
|
+
if (this.cached) {
|
|
92
|
+
if (this.exists(this.cached))
|
|
93
|
+
return this.cached;
|
|
94
|
+
stale = this.cached;
|
|
95
|
+
this.cached = undefined;
|
|
96
|
+
}
|
|
97
|
+
// 1. config 里的 bashPath / DSH_GIT_BASH:显式指定,经同样的校验后缓存。
|
|
98
|
+
const explicit = this.configuredPath;
|
|
99
|
+
if (explicit) {
|
|
100
|
+
this.cached = this.validateExplicit(explicit);
|
|
101
|
+
return this.cached;
|
|
102
|
+
}
|
|
103
|
+
const tried = [];
|
|
104
|
+
if (stale)
|
|
105
|
+
tried.push(`${stale} [used earlier, no longer exists]`);
|
|
106
|
+
for (const candidate of this.candidates()) {
|
|
107
|
+
if (!this.exists(candidate)) {
|
|
108
|
+
tried.push(candidate);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
this.cached = candidate;
|
|
112
|
+
return candidate;
|
|
113
|
+
}
|
|
114
|
+
const error = new Error('dsh-git-bash: no Git Bash found on this machine. Tried:\n' +
|
|
115
|
+
tried.map((line) => ` - ${line}`).join('\n') +
|
|
116
|
+
'\nInstall Git for Windows, or point this plugin at bash.exe explicitly — set the ' +
|
|
117
|
+
'DSH_GIT_BASH environment variable, or write bashPath into the plugin row config.');
|
|
118
|
+
error.code = 'GIT_BASH_NOT_FOUND';
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* 校验显式指定的 bash:路径必须存在且不是 WSL 启动器。
|
|
123
|
+
* 裸名字从 PATH 选择第一个非 WSL 的 bash,返回其绝对路径。
|
|
124
|
+
*/
|
|
125
|
+
validateExplicit(value) {
|
|
126
|
+
if (hasSeparator(value)) {
|
|
127
|
+
if (!this.exists(value))
|
|
128
|
+
throw invalidExplicit(value, 'no such file exists');
|
|
129
|
+
if (this.isWslLauncher(value))
|
|
130
|
+
throw invalidExplicit(value, 'it is the WSL launcher, not Git Bash');
|
|
131
|
+
return value;
|
|
132
|
+
}
|
|
133
|
+
for (const candidate of this.findExecutables(value)) {
|
|
134
|
+
if (this.isWslLauncher(candidate) || !this.exists(candidate))
|
|
135
|
+
continue;
|
|
136
|
+
return candidate;
|
|
137
|
+
}
|
|
138
|
+
throw invalidExplicit(value, 'no Git Bash with that name is on PATH');
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* 是否为 WSL 的 bash 启动器:`%SystemRoot%\System32\bash.exe`,或当前用户的
|
|
142
|
+
* `%LOCALAPPDATA%\Microsoft\WindowsApps\bash.exe`。
|
|
143
|
+
*/
|
|
144
|
+
isWslLauncher(path) {
|
|
145
|
+
const env = this.env;
|
|
146
|
+
const target = canonicalPath(path);
|
|
147
|
+
if (env.SystemRoot && canonicalPath(win32.join(env.SystemRoot, 'System32\\bash.exe')) === target)
|
|
148
|
+
return true;
|
|
149
|
+
if (env.windir && canonicalPath(win32.join(env.windir, 'System32\\bash.exe')) === target)
|
|
150
|
+
return true;
|
|
151
|
+
if (env.LOCALAPPDATA &&
|
|
152
|
+
canonicalPath(win32.join(env.LOCALAPPDATA, 'Microsoft\\WindowsApps\\bash.exe')) === target) {
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
exists(path) {
|
|
158
|
+
try {
|
|
159
|
+
return this.stat(path).isFile();
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
if (error?.code !== 'EACCES')
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
const entry = this.lstat(path);
|
|
167
|
+
return entry.isFile() || entry.isSymbolicLink();
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
findExecutables(name) {
|
|
174
|
+
const pathKey = Object.keys(this.env).find((key) => key.toUpperCase() === 'PATH');
|
|
175
|
+
const path = pathKey === undefined ? '' : (this.env[pathKey] ?? '');
|
|
176
|
+
if (win32.extname(name) && win32.extname(name).toLowerCase() !== '.exe')
|
|
177
|
+
return [];
|
|
178
|
+
const executable = win32.extname(name) ? name : `${name}.exe`;
|
|
179
|
+
const hits = [];
|
|
180
|
+
for (const entry of path.split(';')) {
|
|
181
|
+
const directory = entry.trim().replace(/^"|"$/g, '');
|
|
182
|
+
if (!directory)
|
|
183
|
+
continue;
|
|
184
|
+
const candidate = win32.resolve(directory, executable);
|
|
185
|
+
if (this.exists(candidate))
|
|
186
|
+
hits.push(candidate);
|
|
187
|
+
}
|
|
188
|
+
return hits;
|
|
189
|
+
}
|
|
190
|
+
}
|
package/lib/workdir.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `workdir` 的路径方言。
|
|
3
|
+
*
|
|
4
|
+
* 模型在 bash 输出里看到的是 `/c/...` 这类 MSYS 路径,把它填进 `workdir` 时,
|
|
5
|
+
* Windows 会把 `/c/...` 当成当前盘根目录下的 `c`,spawn 直接 ENOENT;`/tmp`
|
|
6
|
+
* 更糟,它会被当成 `C:\tmp`,命令静默跑到另一个目录。
|
|
7
|
+
*
|
|
8
|
+
* 这里只转换有唯一 Windows 对应物的形式:`/c/...` → `C:/...`。`/tmp`、`/usr`
|
|
9
|
+
* 这类路径取决于 MSYS 的挂载表,没有固定答案,直接报错交给模型改。
|
|
10
|
+
*
|
|
11
|
+
* @module dsh-git-bash/workdir
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* 把 `workdir` 归一成 Windows 路径。非 MSYS 路径原样返回。
|
|
15
|
+
* @param value - `workdir` 原值。
|
|
16
|
+
* @returns Windows 路径,或原值。
|
|
17
|
+
* @throws `workdir` 是 `/tmp` 这类没有盘符的 MSYS 绝对路径时抛出。
|
|
18
|
+
*/
|
|
19
|
+
export declare function normalizeWorkdir(value: string): string;
|
package/lib/workdir.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `workdir` 的路径方言。
|
|
3
|
+
*
|
|
4
|
+
* 模型在 bash 输出里看到的是 `/c/...` 这类 MSYS 路径,把它填进 `workdir` 时,
|
|
5
|
+
* Windows 会把 `/c/...` 当成当前盘根目录下的 `c`,spawn 直接 ENOENT;`/tmp`
|
|
6
|
+
* 更糟,它会被当成 `C:\tmp`,命令静默跑到另一个目录。
|
|
7
|
+
*
|
|
8
|
+
* 这里只转换有唯一 Windows 对应物的形式:`/c/...` → `C:/...`。`/tmp`、`/usr`
|
|
9
|
+
* 这类路径取决于 MSYS 的挂载表,没有固定答案,直接报错交给模型改。
|
|
10
|
+
*
|
|
11
|
+
* @module dsh-git-bash/workdir
|
|
12
|
+
*/
|
|
13
|
+
/** MSYS 盘符路径:`/c`、`/c/foo`。 */
|
|
14
|
+
const MSYS_DRIVE = /^\/([A-Za-z])(?=\/|$)/;
|
|
15
|
+
/**
|
|
16
|
+
* 把 `workdir` 归一成 Windows 路径。非 MSYS 路径原样返回。
|
|
17
|
+
* @param value - `workdir` 原值。
|
|
18
|
+
* @returns Windows 路径,或原值。
|
|
19
|
+
* @throws `workdir` 是 `/tmp` 这类没有盘符的 MSYS 绝对路径时抛出。
|
|
20
|
+
*/
|
|
21
|
+
export function normalizeWorkdir(value) {
|
|
22
|
+
if (!value.startsWith('/'))
|
|
23
|
+
return value;
|
|
24
|
+
// UNC 路径 `//server/share` 在 Windows 上有效,不按 MSYS 路径处理。
|
|
25
|
+
if (value.startsWith('//'))
|
|
26
|
+
return value;
|
|
27
|
+
const match = MSYS_DRIVE.exec(value);
|
|
28
|
+
if (!match) {
|
|
29
|
+
const error = new Error(`dsh-git-bash: workdir "${value}" is an MSYS path with no Windows equivalent; ` +
|
|
30
|
+
'pass a Windows path such as `C:/...`, and use MSYS paths only inside the command string.');
|
|
31
|
+
error.code = 'GIT_BASH_INVALID_WORKDIR';
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
return `${match[1].toUpperCase()}:${value.slice(2) || '/'}`;
|
|
35
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,95 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@idlercloud/dsh-git-bash",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Git for Windows bash tool for DSH on Windows; fork of Fishquito7/dsh-gitbash.",
|
|
5
|
+
"author": "idlercloud",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "lib/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/index.d.ts",
|
|
11
|
+
"default": "./lib/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"dsh": {
|
|
16
|
+
"bundle": {
|
|
17
|
+
"patch": "./cordis.patch.yml",
|
|
18
|
+
"platforms": [
|
|
19
|
+
"win32"
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
"compatibility": {
|
|
23
|
+
"dshReleases": {
|
|
24
|
+
"0.2.2-rc.1": "compatible"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"lib",
|
|
30
|
+
"cordis.patch.yml",
|
|
31
|
+
"README.md",
|
|
32
|
+
"README.en.md"
|
|
33
|
+
],
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": "^22.19 || >=24",
|
|
39
|
+
"dsh": "0.2.2-rc.1"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsc -p tsconfig.json",
|
|
43
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
44
|
+
"pretest": "npm run build",
|
|
45
|
+
"test": "node --test \"test/*.test.mjs\"",
|
|
46
|
+
"pretest:git-bash": "npm run build",
|
|
47
|
+
"test:git-bash": "node test/harness.mjs",
|
|
48
|
+
"lint": "biome lint .",
|
|
49
|
+
"format": "biome format --write .",
|
|
50
|
+
"format:check": "biome format .",
|
|
51
|
+
"check": "biome check .",
|
|
52
|
+
"check:fix": "biome check --write .",
|
|
53
|
+
"prepublishOnly": "npm run check && npm test",
|
|
54
|
+
"prepack": "npm run build"
|
|
55
|
+
},
|
|
56
|
+
"license": "MIT",
|
|
57
|
+
"repository": {
|
|
58
|
+
"type": "git",
|
|
59
|
+
"url": "git+https://github.com/idlercloud/dsh-git-bash.git"
|
|
60
|
+
},
|
|
61
|
+
"homepage": "https://github.com/idlercloud/dsh-git-bash#readme",
|
|
62
|
+
"bugs": {
|
|
63
|
+
"url": "https://github.com/idlercloud/dsh-git-bash/issues"
|
|
64
|
+
},
|
|
65
|
+
"keywords": [
|
|
66
|
+
"dsh",
|
|
67
|
+
"dsh-plugin",
|
|
68
|
+
"deepseek-harness",
|
|
69
|
+
"bash",
|
|
70
|
+
"git-bash",
|
|
71
|
+
"windows"
|
|
72
|
+
],
|
|
73
|
+
"peerDependencies": {
|
|
74
|
+
"@deepseek-ai/cordis": "4.1.0",
|
|
75
|
+
"@deepseek-ai/dsh-bash-local": "0.2.2-rc.1",
|
|
76
|
+
"@deepseek-ai/dsh-scope": "0.2.2-rc.1",
|
|
77
|
+
"@deepseek-ai/schemastery": "3.19.0"
|
|
78
|
+
},
|
|
79
|
+
"devDependencies": {
|
|
80
|
+
"@biomejs/biome": "2.6.6",
|
|
81
|
+
"@deepseek-ai/cordis": "4.1.0",
|
|
82
|
+
"@deepseek-ai/dsh-bash-local": "0.2.2-rc.1",
|
|
83
|
+
"@deepseek-ai/dsh-sandbox": "0.2.2-rc.1",
|
|
84
|
+
"@deepseek-ai/dsh-sandbox-policy": "0.2.2-rc.1",
|
|
85
|
+
"@deepseek-ai/dsh-scope": "0.2.2-rc.1",
|
|
86
|
+
"@deepseek-ai/dsh-shell": "0.2.2-rc.1",
|
|
87
|
+
"@deepseek-ai/dsh-system-prompt": "0.2.2-rc.1",
|
|
88
|
+
"@deepseek-ai/dsh-tool-bash": "0.2.2-rc.1",
|
|
89
|
+
"@deepseek-ai/dsh-tools": "0.2.2-rc.1",
|
|
90
|
+
"@deepseek-ai/schemastery": "3.19.0",
|
|
91
|
+
"@types/node": "^22.20.5",
|
|
92
|
+
"typescript": "^5.9.3"
|
|
93
|
+
},
|
|
94
|
+
"types": "lib/index.d.ts"
|
|
95
|
+
}
|