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 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.2",
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.2",
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
- 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)