dsh-gitbash-shell 0.40.2 → 0.40.4
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/CHANGELOG.md +23 -0
- package/dsh.plugin.json +1 -1
- package/package.json +1 -1
- package/src/index.js +14 -5
- package/src/toolchain.js +19 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,29 @@
|
|
|
3
3
|
> 倒序排列,新版本条目在最上面。条目格式:`## vX.Y.Z — YYYY-MM-DD` + 类型(feat / fix / docs / chore)+ 要点 + 相关链接。
|
|
4
4
|
> 纪律见 AGENTS.md「变更记录纪律」:发版前先更新本文件并随版本提交;事故复盘、复现与真机验证记录也记在这里。
|
|
5
5
|
|
|
6
|
+
## v0.40.4 — 2026-10-17
|
|
7
|
+
|
|
8
|
+
### 终端工具扫描只认可执行条目:手册页/文档/同名目录不再冒充命令(fix)
|
|
9
|
+
|
|
10
|
+
- `scanCommands`(`src/toolchain.js`)原先用 `entry.replace(/\.[^./\\]+$/, '')` 剥掉**任意**扩展名,于是 PATH 目录里任何与工具同名的条目都算命中:
|
|
11
|
+
- **真实案例**:winget 便携包目录中 `just.1`(手册页)在字典序上排在 `just.exe` 之前,而扫描是「首个命中者胜出」⇒ 卡片「完整路径 / 由谁提供」列把手册页当成了 `just` 的可执行文件(实测 `commandPath` = `…\Casey.Just_Microsoft.Winget.Source_8wekyb3d8bbwe\just.1`)。
|
|
12
|
+
- **判定层影响**:`classifyToolState` 在 `wingetOwned === false` 时只看 `hasCommand`,所以一台**确实没装**该命令、而 PATH 里恰好有同名文档 / 手册页 / 目录的机器,该行会被判成 `external`(`isActionable` 为 false)——**安装入口永不出现**。本机 `just` 之所以未暴露,只是 winget 清单命中在前把它掩盖了。
|
|
13
|
+
- 修法(**纯函数改动、零 io 变更**):新增扩展名白名单 `EXEC_EXTS = {.exe,.cmd,.bat,.com}`,扩展名不在白名单的条目直接跳过;**无扩展名照旧命中**——Git for Windows 自带 `/usr/bin/make` 这类无扩展名可执行文件(`fzf` 同理),这条不能丢。未加 `.` / `..` 防御分支:`readdirSync` 本就不返回这两项。
|
|
14
|
+
- 回归用例(`tests/smoke.mjs`「only executable extensions resolve」)锁三层:手册页 / 文档 / 目录不命中;无扩展名真可执行仍命中;`just.1` 排在 `just.exe` 之前时**首个可执行命中者**路径正确(另覆盖 `.EXE` 大小写)。判定层补一条联动断言:扫不到 ⇒ `missing` ⇒ `isActionable` 为真。**该用例已在未修复的源码上验证会失败**(`actual: 'C:/pkg\just.1'`),修复后通过。
|
|
15
|
+
- 目录区分(无扩展名目录 vs 无扩展名 MSYS 可执行文件)**留作后续**:需要给 `io.readdir` 增加 `stat`/`Dirent` 能力并牵动调用方,本版不动 io 契约。
|
|
16
|
+
- 验证:Windows 10.0.26200 / Node 24.19.0,`npm test` 通过(当时基线计数把 skip 计入记作 237;维护者复核:基线 235 pass + 1 skip,合并分支 236 pass + 1 skip,本用例恰好多 1)。维护者另在 macOS / Node 24 复跑全量同绿,CI ubuntu+windows × Node 22/24 四矩阵绿。
|
|
17
|
+
- 相关:[issue #31](https://github.com/KannaKuron/dsh-gitbash-shell/issues/31)。
|
|
18
|
+
|
|
19
|
+
## v0.40.3 — 2026-10-17
|
|
20
|
+
|
|
21
|
+
### gitBash 能力服务新增 `pwshToolEnabled`(feat,additive——配对 dsh-ptc-cordis-preset v0.20.1)
|
|
22
|
+
|
|
23
|
+
- **背景**:PTC 创造模式(ptc-cordis preset)的 pwsh 可见性矩阵有个残余不一致——gitbash-shell 单独安装且用户开启 `pwshTool` 开关时,官方 PTC/标准模式有 pwsh(遮蔽层放行、翻译层保护),而 ptc-cordis 组合仍禁(v0.20.0 只探测 dsh-ops 的 `opsPresent`,看不到本插件的私有设置)。跨插件没有读对方 config 的通道,唯一干净做法是把判定结果发布在已有的 `gitBash` 能力服务上。
|
|
24
|
+
- **改动**:`gitBashCapability` 新增 `get pwshToolEnabled()` —— 返回 `liveSettings.pwshTool() === true`,与遮蔽层(`src/inline-tools.js` 的 `pwshVisible` 判定)**同一读取器、同一语义**:能力字段回答的就是「pwsh 行此刻对本机 agent 是否可见」。纯 additive(先例:`ok/source` v0.28.0、`inlinePatched` v0.35.1),老消费方忽略新字段。
|
|
25
|
+
- **接线重排**:`const liveSettings = makeLiveReader(ctx, config)` 从能力服务之后提升到之前——getter 共享同一 reader,且声明先于能力对象构造,杜绝任何同步读取路径上的 TDZ。
|
|
26
|
+
- **测试**:smoke 源码断言补 getter 形状 + liveSettings 必须先于 `gitBashCapability` 声明(防 TDZ 复发)。236 tests / 235 pass / 1 skip / 0 fail。
|
|
27
|
+
- **消费方**:dsh-ptc-cordis-preset v0.20.1 起,组合条件从 `gitBashActive && !opsPresent` 扩为再 `&& !peerPwshTool`,「手动开 pwshTool、不装 dsh-ops」场景下 PTC 创造与官方模式一致放行。
|
|
28
|
+
|
|
6
29
|
## v0.40.2 — 2026-10-17
|
|
7
30
|
|
|
8
31
|
### 适配 dsh 0.2.1-alpha.5(0.2.1-rc.1 与其同码):组合对齐重跑 + exit-status 解析器跟齐(feat / fix / test)
|
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-external/dsh-gitbash-shell",
|
|
3
|
-
"version": "0.40.
|
|
3
|
+
"version": "0.40.4",
|
|
4
4
|
"main": "./src/index.js",
|
|
5
5
|
"description": "Windows 全模式 Git Bash:自带 bash 工具(私有 Git Bash 执行器实例,官方 PowerShell 执行器原样保留),官方四个模式经行内工具补位直接获得 Git Bash(v0.35.0 起;v0.39.0 起不再替换宿主执行器),四个 *·Git Bash 变体 preset 可开关;官方 pwsh 工具默认隐藏、开关可见(真 PowerShell);附带 gb_* FastCtx 文件工具,检测到 dsh-ops 自动让位。联动:dsh-ptc-cordis-preset(PTC 创造模式带 Git Bash 行且可去重)+ dsh-better-sidebar(终端标签自动切 Git Bash)。",
|
|
6
6
|
"engines": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-gitbash-shell",
|
|
3
|
-
"version": "0.40.
|
|
3
|
+
"version": "0.40.4",
|
|
4
4
|
"description": "Windows 上给 agent 一个真正的 Git Bash 命令工具:私有 Git Bash 执行器实例(官方 PowerShell 执行器原样保留),统一 MSYS 盘根路径方言,官方 pwsh 工具默认隐藏可开关,物化四个内置预设的 Git Bash 变体,侧栏终端接管可开关。· Give agents a real Git Bash tool on Windows: a private Git Bash executor instance (the official PowerShell executor stays untouched), a unified MSYS drive-root path dialect, an opt-in official pwsh tool, and four Git Bash preset variants.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
package/src/index.js
CHANGED
|
@@ -2106,6 +2106,13 @@ export async function apply(ctx, config = {}) {
|
|
|
2106
2106
|
console.log(`${TAG} inspect-registry shim wiring failed: ${error?.message ?? error}`)
|
|
2107
2107
|
}
|
|
2108
2108
|
|
|
2109
|
+
// Era-aware live settings: every consumer below re-reads the row Config
|
|
2110
|
+
// refs on every use (volatile), so a flipped knob lands on the next
|
|
2111
|
+
// dispatch with zero wiring. Declared BEFORE the cooperation capability so
|
|
2112
|
+
// its getters share the same reader (v0.40.3 pwshToolEnabled) — the
|
|
2113
|
+
// capability must answer with the exact value the masking layer sees.
|
|
2114
|
+
const liveSettings = makeLiveReader(ctx, config)
|
|
2115
|
+
|
|
2109
2116
|
// ── cooperation capability ────────────────────────────────────────────────
|
|
2110
2117
|
// Publish whether the Git Bash shell stack is active on this host so peer
|
|
2111
2118
|
// plugins (e.g. dsh-ptc-cordis-preset) can adopt it in the presets they
|
|
@@ -2140,6 +2147,13 @@ export async function apply(ctx, config = {}) {
|
|
|
2140
2147
|
// anything — peers reading this flag drop the suffix (dsh-ptc-cordis-preset
|
|
2141
2148
|
// v0.17.3+); older peers ignore it and keep the v0.34 wording.
|
|
2142
2149
|
get inlinePatched() { return process.platform === 'win32' },
|
|
2150
|
+
// Additive (v0.40.3): whether the official pwsh row is VISIBLE for agents
|
|
2151
|
+
// on this host — the exact `pwshTool() === true` verdict the masking layer
|
|
2152
|
+
// reads (src/inline-tools.js hidePwsh), so peers composing the same row
|
|
2153
|
+
// key off one signal instead of re-deriving the switch. Default OFF
|
|
2154
|
+
// (bash-first); the switch is live, so a flip lands on the next read.
|
|
2155
|
+
// Older peers ignore the field and keep the v0.20.0 ops-only behavior.
|
|
2156
|
+
get pwshToolEnabled() { return liveSettings.pwshTool() === true },
|
|
2143
2157
|
// Kept for existing consumers (peer plugins read `bashPath`): the resolved
|
|
2144
2158
|
// interpreter. When resolution failed we report the explicit value (or the
|
|
2145
2159
|
// historical default) so the ENOENT a consumer may hit names a real path —
|
|
@@ -2159,11 +2173,6 @@ export async function apply(ctx, config = {}) {
|
|
|
2159
2173
|
const disposeGitBash = ctx.provide('gitBash', gitBashCapability)
|
|
2160
2174
|
ctx.effect(() => disposeGitBash, 'dsh-gitbash-shell: gitBash capability')
|
|
2161
2175
|
|
|
2162
|
-
// Era-aware live settings: every consumer below re-reads the row Config
|
|
2163
|
-
// refs on every use (volatile), so a flipped knob lands on the next
|
|
2164
|
-
// dispatch with zero wiring.
|
|
2165
|
-
const liveSettings = makeLiveReader(ctx, config)
|
|
2166
|
-
|
|
2167
2176
|
// ── official shell-env fact: DSH_PATH_DIALECT (Windows only, gated; v0.11.0) ──
|
|
2168
2177
|
// dsh-shell-env owns the model-visible $DSH_* facts (host-plane service;
|
|
2169
2178
|
// the web composition injects it and publishes DSH_WEB_URL beside the
|
package/src/toolchain.js
CHANGED
|
@@ -213,18 +213,30 @@ export function parseExportJson(text, tools) {
|
|
|
213
213
|
return found
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
+
/**
|
|
217
|
+
* The extensions a PATH entry must carry to BE the command. Anything else in a
|
|
218
|
+
* PATH directory — a man page, a README, a directory — merely shares the stem
|
|
219
|
+
* and must never resolve. An entry with NO extension still does: Git for
|
|
220
|
+
* Windows ships its own executables that way (`/usr/bin/make`).
|
|
221
|
+
*/
|
|
222
|
+
const EXEC_EXTS = new Set(['.exe', '.cmd', '.bat', '.com'])
|
|
223
|
+
|
|
216
224
|
/**
|
|
217
225
|
* Which catalog commands the machine already has, and WHERE they came from.
|
|
218
226
|
* One `readdir` per PATH directory (not one `exists` per tool × directory ×
|
|
219
227
|
* extension), and every unreadable directory is skipped rather than fatal.
|
|
220
228
|
* First PATH hit wins, which is the same precedence the shell itself applies.
|
|
229
|
+
* Only EXECUTABLE entries resolve (see EXEC_EXTS): a man page (`just.1`) or a
|
|
230
|
+
* document (`jq.md`) shares the stem without being the command, and issue #31
|
|
231
|
+
* measured a man page winning the first-hit race over the `just.exe` beside it.
|
|
221
232
|
* @param {{pathDirs: () => string[], readdir: (dir: string) => string[]}} io
|
|
222
233
|
* @param {Array} tools - catalog entries to look for.
|
|
223
234
|
* @returns {Map<string, string>} cmd → the first resolving full path.
|
|
224
235
|
*/
|
|
225
236
|
export function scanCommands(io, tools) {
|
|
226
237
|
const list = Array.isArray(tools) ? tools : TOOLCHAIN
|
|
227
|
-
// "make.exe" / "make.cmd" / "make" all resolve the command `make
|
|
238
|
+
// "make.exe" / "make.cmd" / "make" all resolve the command `make`; "make.1"
|
|
239
|
+
// (the man page) and "make.txt" do not — see EXEC_EXTS.
|
|
228
240
|
const wanted = new Map(list.map((tool) => [String(tool.cmd).toLowerCase(), tool.cmd]))
|
|
229
241
|
const found = new Map()
|
|
230
242
|
let dirs
|
|
@@ -236,7 +248,12 @@ export function scanCommands(io, tools) {
|
|
|
236
248
|
if (!Array.isArray(entries)) continue
|
|
237
249
|
for (const entry of entries) {
|
|
238
250
|
if (typeof entry !== 'string') continue
|
|
239
|
-
|
|
251
|
+
// The LAST extension decides, exactly as before — but it now has to be
|
|
252
|
+
// an executable one, so a man page/doc/directory is skipped instead of
|
|
253
|
+
// resolving to a command it merely shares a stem with.
|
|
254
|
+
const suffix = /(\.[^./\\]+)$/.exec(entry)
|
|
255
|
+
if (suffix !== null && !EXEC_EXTS.has(suffix[1].toLowerCase())) continue
|
|
256
|
+
const base = (suffix === null ? entry : entry.slice(0, suffix.index)).toLowerCase()
|
|
240
257
|
const cmd = wanted.get(base)
|
|
241
258
|
if (cmd !== undefined && !found.has(cmd)) {
|
|
242
259
|
found.set(cmd, dir.replace(/[\\/]+$/, '') + '\\' + entry)
|