dsh-gitbash-shell 0.40.3 → 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 CHANGED
@@ -3,6 +3,19 @@
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
+
6
19
  ## v0.40.3 — 2026-10-17
7
20
 
8
21
  ### gitBash 能力服务新增 `pwshToolEnabled`(feat,additive——配对 dsh-ptc-cordis-preset v0.20.1)
package/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-external/dsh-gitbash-shell",
3
- "version": "0.40.3",
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",
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/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
- const base = entry.replace(/\.[^./\\]+$/, '').toLowerCase()
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)