dsh-gitbash-shell 0.32.1 → 0.33.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/CHANGELOG.md +37 -0
- package/README.md +11 -3
- package/README_EN.md +8 -1
- package/dsh.plugin.json +1 -1
- package/package.json +1 -1
- package/src/bash-path.js +120 -17
- package/src/index.js +112 -21
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,43 @@
|
|
|
3
3
|
> 倒序排列,新版本条目在最上面。条目格式:`## vX.Y.Z — YYYY-MM-DD` + 类型(feat / fix / docs / chore)+ 要点 + 相关链接。
|
|
4
4
|
> 纪律见 AGENTS.md「变更记录纪律」:发版前先更新本文件并随版本提交;事故复盘、复现与真机验证记录也记在这里。
|
|
5
5
|
|
|
6
|
+
## v0.33.0 — 2026-10-01
|
|
7
|
+
|
|
8
|
+
**类型**:fix(issue #13:Scoop 安装 Git 时 `bashPath` 配置正确但每次启动仍报 could NOT be resolved 并弹引导框)+ feat(解析链新增 `git --exec-path` 与 GitForWindows 注册表两级发现)
|
|
9
|
+
|
|
10
|
+
> 报告者 **ID-izlq-Github** 的定位是准确的:装配期冻结、两个读取器在 apply 时刻读空、自动链对 shim 布局盲区,三条全部成立。本条目按他的分析与建议落地(建议 1/2/3/4 全采纳)。
|
|
11
|
+
|
|
12
|
+
### 修复一:显式层懒化,不再在装配期冻结(`src/index.js`)
|
|
13
|
+
|
|
14
|
+
- **冻结点**(issue 引用的 `index.js:1813`):apply() 时 `effectiveConfiguredBashPath(executorConfiguredBashPath(ctx), settingsBashPath(ctx))` 算一次就定死。那一刻 `ctx.loader.resolve('gitbash-executor')` 因行未进 store 而 throw → 被吞成 `''`;settings 服务也可能尚未注入(0.1.7+ 只有 describe()/update())。两者皆空 ⇒ 误判「未配置」⇒ 跑自动链。执行器(`shell.js` 的 `get bashPath()` 懒解析)却正常 —— **同一次启动、两条路径、相反结论**,这正是 issue 的内核。
|
|
15
|
+
- **修法**:冻结值换成两个读取器 `readConfiguredBashPath()` / `readBashResolution()`,**全部消费点逐次重读**:① `gitBash` capability 改 getter(bashPath/ok/source/configured/tried;ptc 侧只读静态 `active`,不受影响);② status 路由实时重读(弹窗因此不再误报 —— 行挂载完成后 `configured` 非空,解析成功,`ok:true`,弹窗根本不出现);③ pathmap 路由与 tools/execute / post-execute 的 `buildTranslateEnv`;④ 官方侧栏终端接管每次尝试重读;⑤ better-sidebar 接管改挂 settle 通知。
|
|
16
|
+
- **settings 绕行退役**:`gitbash-shell` 行的 `bashPath` 直接从 apply() 的 `config` 参数读(新 `rowBashPath(config)`:volatile ref / 普通值 / 缺席三态,绝不抛)—— 这就是同一行的值,比绕 `settings.describe()` 更直接,且 volatile 意味着用户改设置下一读即生效。宿主半不再 import `settingsBashPath`(shell.js 侧保留:executor 行运行时读 describe() 实测正常)。
|
|
17
|
+
- **启动日志 settle 化**:boot 打印一次;「失败且显式层为空」(行可能还在挂载)时轮询 250ms×20(仅 win32,5 秒封顶),显式层出现或解析翻转后补打 `(settled)` 段;成功或显式值被拒为最终态,不轮询。terminal 接管通过 `onBashSettled` 通知(better-sidebar 在 boot 失败→settled 成功时也能接管,无需重启)。
|
|
18
|
+
|
|
19
|
+
### 修复一·续:executor 行读取在 0.2.0-rc.2 上恒断,不只是时序(`src/bash-path.js`)
|
|
20
|
+
|
|
21
|
+
- **真机实测(隔离实例 + 探针插件,报告场景复现)**:dsh 0.2.0-rc.2 上 bundle-patch insert 的行(`gitbash-executor`)挂在**所属插件的 subtree** 里,`loader.resolve('gitbash-executor')` 从 root 树查找**恒 throw**(探针 apply 时刻与 12 秒后各 7 次采样全部 throw)——报告者以为的「启动时序问题」在这台宿主上实际是**永远读不到**。旧代码把 throw 吞成 `''` ⇒ 显式层整个失效:patch 写 `gitbash-executor.config.bashPath` 的用户(issue #11 起推荐、#13 报告者的实际配置方式)在 0.2.0 上 boot/status/翻译层全部看不到该值,而执行器自己读 `this.config.bashPath` 正常 —— 「命令能用但每次启动报错+弹框」的另一半根因。**并连带违反 §4h 契约**:显式值(哪怕被拒)不得被自动链顶替 —— 实测坏值 `Q:/nope/bin/bash.exe` 被自动链的 default 层「顶替成功」,boot 谎报 resolved。
|
|
22
|
+
- **修法**:`executorConfiguredBashPath` 二段式 —— `resolve('gitbash-executor')` 先试(旧宿主行在 root 树,命中即返回,空串也是真实答案=自动链);throw 则 `loader.entries()` 迭代(**含 subtree**)按行自身 `options.id` 匹配(0.2.0 实测稳定可读,`bashPath` 值全程不变)。防回归用例锁死三种 loader 形状(root 命中 / subtree+entries / 行缺失)。
|
|
23
|
+
- **修复后 A/B 真机读数**(隔离实例,patch 写 `gitbash-executor.config.bashPath`):
|
|
24
|
+
- 坏值 `Q:/nope/...`:boot `bashPath is set to "Q:/nope/bin/bash.exe" and that value was rejected; nothing else is substituted`(fail-loud、无轮询、default 层不再顶替);status `ok:false, configured:"Q:/nope/bin/bash.exe", tried[0]={source:'configured',code:'missing'}` —— boot/status/执行器三方一致;
|
|
25
|
+
- 好值:boot `Git Bash resolved from the configured source`;status `ok:true, source:"configured"` —— 弹窗不出现。
|
|
26
|
+
|
|
27
|
+
### 修复二:解析链补 shim 布局的发现方式(`src/bash-path.js`)
|
|
28
|
+
|
|
29
|
+
- **新 source `exec-path`**(位置:PATH 反推之后、注册表 PATH 之前):对 PATH 上第一个 `git.exe` 跑 `git --exec-path`(Scoop/choco 的 **shim 也能启动真身**,真身报告自己的家),从输出向上最多 5 级找「同时含 `cmd/git.exe` 与 `usr/bin/bash.exe`」的目录,产出 `<root>/bin/bash.exe`。纯函数 `execPathBashCandidates(execPath, exists)` 可测;`ucrt64`/`mingw64`/旧 flat `libexec` 三种布局全覆盖,MSYS2(`usr/libexec/git-core`,根上没有 `cmd/git.exe`)天然不产出候选。
|
|
30
|
+
- **GitForWindows 注册表兜底**:`HKLM\SOFTWARE\GitForWindows`、`HKLM\SOFTWARE\WOW6432Node\GitForWindows`、`HKCU\Software\GitForWindows` 的 `InstallPath`(官方安装器写的;Scoop/便携通常没有,故排在 exec-path 之后),同样走 `exec-path` source。
|
|
31
|
+
- **候选仍过全套判据**(黑名单 → 形状 → `cmd/git.exe`/`usr/bin/bash.exe` → mingw64/msys-2.0.dll → `.windows.` 版本指纹 → `uname -s` 一票否决):§4h 两条硬边界不变 —— 永不回退、只认 Git for Windows 的 bash。冒烟新增「WSL bash 经 exec-path 到达仍被拒」用例。
|
|
32
|
+
- **顺带**:`defaultIo().run` 从 execFileSync 换 spawnSync(piped stdio)—— execFileSync 的同步家族在子进程失败时把 stderr 直接泄漏到宿主日志(reg 的 GBK 错误文本 = 乱码),v0.28.0 起每次失败探测都在漏;spawnSync 捕获进 result 由 catch 丢弃。本机实测乱码消除、探测结果不变(exec-path 层产出 `C:/Program Files/Git/bin/bash.exe`,mingw64 布局反推正确,全链仍 `source=default` 先命中,新层是后备)。
|
|
33
|
+
|
|
34
|
+
### 冒烟与真机
|
|
35
|
+
|
|
36
|
+
- 新增三组用例:①「exec-path covers Scoop layouts」(纯函数三布局 + MSYS2 不命中 + 全链 Scoop 场景 + WSL 否决);②「explicit tier is LAZY」(冻结模式防回归:断言 `const configuredBashPath = effectiveConfiguredBashPath(` 与 `const bashResolution = resolveGitBashCached(` 不得再现;懒读取器/status 实时重读/settle 通知必须存在;`rowBashPath` 三态);③「executor row lookup survives the subtree layout change」(loader 三形状:root 命中 / subtree+entries / 行缺失,空串=真实答案)。
|
|
37
|
+
- **顺手修掉存量失败**(0.32.1 在 win32 上 latent):「full apply() mount on a stub ctx」的 settings stub 还停在 `settings.get(ns)` 时代,readShell 自 v0.25.1 起只认 `describe()` ⇒ stub 恒 null ⇒ 轮询 12×1500ms 后断言必挂。stub 补 `describe()`,与现役读法对齐。
|
|
38
|
+
- 99 项全绿(96 存量 + 3 新增;toolchain PATH 用例的平台误判 v0.32.1 已修,本次全平台绿)。
|
|
39
|
+
- **真机验证**(隔离 `DSH_HOME`(Temp 目录)+ profile 副本 `cp -rL` 解引用 + 插件目录换本仓源码 + 探针插件采 loader 形状;全程独占 3080 —— 用户实例当时未运行,验证后实例已杀、410M 副本与探针已删净,`~/.dsh` 零触碰):默认场景 boot `resolved from the default source`、status `ok:true`;坏值/好值 A/B 见上文「修复一·续」;官方侧栏终端接管懒读路径正常(`already uses this Git Bash ... (nothing written)` 幂等);ptc 联动正常(cordis-gitbash 去重、对方注册 Git Bash rows —— capability getter 无碍);spawnSync 改造后启动日志**零乱码**。**未覆盖**:settle 轮询分支的翻转真机时序(0.2.0-rc.2 上 entries() 在 apply 时刻即稳定可读,轮询多数情况下不触发;接线由源码断言锁定,行为留待真实慢宿主检验)。
|
|
40
|
+
|
|
41
|
+
- 相关:issue #13(<https://github.com/KannaKuron/dsh-gitbash-shell/issues/13>)、issue #11(解析链的由来)、AGENTS.md §4h(顺序契约更新)。
|
|
42
|
+
|
|
6
43
|
## v0.32.1 — 2026-09-28
|
|
7
44
|
|
|
8
45
|
**类型**:chore(清掉 v0.32.0 遗留的 legacy settings 死代码)+ fix(冒烟里 mac 平台误判的 toolchain 路径用例)
|
package/README.md
CHANGED
|
@@ -126,7 +126,7 @@ dsh-ptc-cordis-preset 报告它的 `PTC 创造模式` 已经是 Git Bash 版。
|
|
|
126
126
|
default: ptc-cordis # 或 standard-gitbash / minimal-gitbash
|
|
127
127
|
```
|
|
128
128
|
|
|
129
|
-
## bash 从哪里来:解析链与失败引导(v0.28.0,issue #11)
|
|
129
|
+
## bash 从哪里来:解析链与失败引导(v0.28.0,issue #11;懒化与 exec-path 层 v0.33.0,issue #13)
|
|
130
130
|
|
|
131
131
|
`bashPath` **默认留空 = 自动探测**;解析顺序就是契约:
|
|
132
132
|
|
|
@@ -136,8 +136,16 @@ dsh-ptc-cordis-preset 报告它的 `PTC 创造模式` 已经是 Git Bash 版。
|
|
|
136
136
|
`%LOCALAPPDATA%\Programs\Git\bin\bash.exe`;
|
|
137
137
|
3. **PATH**(用户 PATH + 系统 PATH,进程内已合并)逐目录找 `bash.exe`;
|
|
138
138
|
4. **PATH 上的 `git.exe` 反推** `<gitdir>/../bin/bash.exe`;
|
|
139
|
-
5.
|
|
140
|
-
|
|
139
|
+
5. **`git --exec-path` 反推 + GitForWindows 注册表**(v0.33.0):对 PATH 上的 `git.exe`(Scoop / choco 的
|
|
140
|
+
**shim** 也照常启动真身)跑 `--exec-path`,从输出向上找 Git 根 —— 兼容 `mingw64` / `ucrt64` 两种运行时
|
|
141
|
+
布局,补上「Scoop 安装、PATH 上只有 shims、没有 `bash.exe`」的盲区;随后兜底读注册表
|
|
142
|
+
`HKLM\SOFTWARE\GitForWindows` 的 `InstallPath`;
|
|
143
|
+
6. **注册表 `Path`**(`HKCU\Environment`、`HKLM\...\Session Manager\Environment`)——覆盖"GUI 启动时 PATH 快照过期";
|
|
144
|
+
7. **全落空 ⇒ 报错 + 引导**(启动日志一份完整报告,客户端弹一次引导框)。
|
|
145
|
+
|
|
146
|
+
**解析结果是活的,不是启动时判一次**(v0.33.0,issue #13):启动那一刻插件行可能还没全部挂载,显式配置
|
|
147
|
+
会被短暂读成空 —— 现在启动日志、状态接口、弹窗与侧栏终端接管都在**使用时实时重读**,执行器能用,
|
|
148
|
+
它们就不会再说「解析失败」;启动日志也会在行挂载稳定后补一份最终结论。
|
|
141
149
|
|
|
142
150
|
> **只认 Git for Windows 的 bash。** WSL(`C:\Windows\System32\bash.exe`,永远在 PATH 且排序靠前)、
|
|
143
151
|
> WindowsApps 别名、MSYS2、Cygwin 的 bash **一律拒绝**;候选还要通过 Git 布局(`usr/bin` + `cmd\git.exe` +
|
package/README_EN.md
CHANGED
|
@@ -91,9 +91,16 @@ Executor config:
|
|
|
91
91
|
|
|
92
92
|
```yaml
|
|
93
93
|
config:
|
|
94
|
-
bashPath: "D:/Tools/Git/bin/bash.exe" # default
|
|
94
|
+
bashPath: "D:/Tools/Git/bin/bash.exe" # default '' (empty) = automatic chain
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
+
The automatic chain (empty `bashPath`) probes, in order: explicit settings → default
|
|
98
|
+
install locations → PATH → `git.exe` reverse lookup → `git --exec-path` (covers
|
|
99
|
+
Scoop/choco shim layouts and the `ucrt64` runtime, v0.33.0) + the GitForWindows
|
|
100
|
+
registry `InstallPath` → the registry `Path`. Resolution is re-read live per
|
|
101
|
+
consumer (never frozen at boot, v0.33.0), and only Git for Windows' own bash
|
|
102
|
+
counts — WSL / MSYS2 / Cygwin are refused with guidance, never substituted.
|
|
103
|
+
|
|
97
104
|
## Cooperation with dsh-ptc-cordis-preset
|
|
98
105
|
|
|
99
106
|
This plugin publishes a `gitBash` host capability (`{ active, bashPath }`,
|
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-external/dsh-gitbash-shell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.0",
|
|
4
4
|
"main": "./src/index.js",
|
|
5
5
|
"description": "Windows 全模式 Git Bash:用 Git for Windows bash 替换 PowerShell 执行器,声明式注册 standard/minimal/code/cordis 的 Git Bash 变体 preset(preset id 永不变)。联动:dsh-ptc-cordis-preset(两者同装时 PTC 创造模式带 Git Bash 行且可去重)+ dsh-better-sidebar(终端 UI 标签与模型 terminal_* 工具自动切到 Git Bash)。",
|
|
6
6
|
"engines": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-gitbash-shell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.0",
|
|
4
4
|
"description": "Windows 上让 agent 的每条 shell 命令改经 Git for Windows bash 执行;物化四个内置预设的 Git Bash 变体,统一 MSYS 盘根路径方言,侧栏终端接管可开关。· Run every agent shell command through Git for Windows bash on Windows; materialize Git Bash variants of the built-in presets, unify the MSYS drive-root path dialect, and adopt the sidebar terminal behind a switch.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
package/src/bash-path.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* arrives through `io`, so the whole chain is unit-testable off-Windows.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
import {
|
|
23
|
+
import { spawnSync } from 'node:child_process'
|
|
24
24
|
import { existsSync } from 'node:fs'
|
|
25
25
|
import { dirname } from 'node:path'
|
|
26
26
|
|
|
@@ -44,7 +44,7 @@ export const GIT_WINDOWS_VERSION_MARKER = '.windows.'
|
|
|
44
44
|
export const GIT_BASH_UNAME = /^MINGW(32|64)_NT-/
|
|
45
45
|
|
|
46
46
|
/** Exit codes/sources reported back to the caller. */
|
|
47
|
-
export const BASH_SOURCES = ['configured', 'default', 'path', 'path-git', 'registry']
|
|
47
|
+
export const BASH_SOURCES = ['configured', 'default', 'path', 'path-git', 'exec-path', 'registry']
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
50
|
* Normalize a Windows path for comparison: forward slashes, lowercase, no
|
|
@@ -132,11 +132,46 @@ export function classifyBashCandidate(candidate, io) {
|
|
|
132
132
|
return { ok: true, path: candidate, root, gitExe }
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Walk UP from a `git --exec-path` output looking for the Git for Windows
|
|
137
|
+
* root, and return its `<root>/bin/bash.exe` candidate (issue #13).
|
|
138
|
+
*
|
|
139
|
+
* Why this tier exists: package managers that SHIM Git (Scoop, and portable
|
|
140
|
+
* installs generally) put `git.exe` on PATH in a directory (`E:/Scoop/shims`)
|
|
141
|
+
* that is neither `<root>/cmd` nor `<root>/bin`, so the PATH reverse lookup
|
|
142
|
+
* never fires and there is no `bash.exe` on PATH at all. The REAL git still
|
|
143
|
+
* knows where it lives — `git --exec-path` prints
|
|
144
|
+
* `<root>/<mingw64|ucrt64>/libexec/git-core` (or `<root>/libexec/git-core`
|
|
145
|
+
* on old layouts) even when launched through a shim — so climbing ≤5 levels
|
|
146
|
+
* and demanding BOTH `cmd/git.exe` and `usr/bin/bash.exe` finds the root on
|
|
147
|
+
* every layout. That pair is also the discriminator: MSYS2's git reports
|
|
148
|
+
* `<msys64>/usr/libexec/git-core`, whose root has `usr/bin/bash.exe` but NO
|
|
149
|
+
* `cmd/git.exe`, so it never produces a candidate here (and even if it did,
|
|
150
|
+
* classifyBashCandidate's blacklist + fingerprints would refuse it).
|
|
151
|
+
* @param {string|undefined} execPath - raw `git --exec-path` output.
|
|
152
|
+
* @param {(path: string) => boolean} exists - filesystem probe.
|
|
153
|
+
* @returns {string[]} zero or one candidate.
|
|
154
|
+
*/
|
|
155
|
+
export function execPathBashCandidates(execPath, exists) {
|
|
156
|
+
if (typeof execPath !== 'string' || execPath.trim() === '') return []
|
|
157
|
+
let dir = execPath.replace(/\\/g, '/').replace(/\/+$/, '')
|
|
158
|
+
for (let i = 0; i < 5; i++) {
|
|
159
|
+
const parent = dirname(dir)
|
|
160
|
+
if (parent === dir || parent === '' || parent === '.') break
|
|
161
|
+
dir = parent
|
|
162
|
+
if (exists(dir + '/cmd/git.exe') && exists(dir + '/usr/bin/bash.exe')) {
|
|
163
|
+
return [dir + '/bin/bash.exe']
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return []
|
|
167
|
+
}
|
|
168
|
+
|
|
135
169
|
/**
|
|
136
170
|
* The ordered candidate chain for one resolution, WITHOUT validation. Order is
|
|
137
171
|
* the contract (issue #11): explicit setting → default install locations →
|
|
138
|
-
* PATH → git.exe reverse lookup →
|
|
139
|
-
* the process PATH snapshot predates a user's
|
|
172
|
+
* PATH → git.exe reverse lookup → git --exec-path / GitForWindows registry
|
|
173
|
+
* (the last one only helps when the process PATH snapshot predates a user's
|
|
174
|
+
* PATH edit).
|
|
140
175
|
* @param {object} input - resolution input.
|
|
141
176
|
* @param {string} input.configured - explicit path ('' = none).
|
|
142
177
|
* @param {object} io - injected facts.
|
|
@@ -152,6 +187,7 @@ export function bashCandidates({ configured }, io) {
|
|
|
152
187
|
for (const path of io.defaults()) out.push({ path, source: 'default' })
|
|
153
188
|
for (const path of io.pathBashCandidates()) out.push({ path, source: 'path' })
|
|
154
189
|
for (const path of io.gitReverseCandidates()) out.push({ path, source: 'path-git' })
|
|
190
|
+
for (const path of io.execPathBashCandidates()) out.push({ path, source: 'exec-path' })
|
|
155
191
|
for (const path of io.registryBashCandidates()) out.push({ path, source: 'registry' })
|
|
156
192
|
return out
|
|
157
193
|
}
|
|
@@ -262,23 +298,50 @@ export function effectiveConfiguredBashPath(ownRowValue, settingsValue) {
|
|
|
262
298
|
|
|
263
299
|
/**
|
|
264
300
|
* The `gitbash-executor` ROW's own `bashPath` — the highest-priority tier.
|
|
265
|
-
* Read through the Loader's public
|
|
301
|
+
* Read through the Loader's public surface (the row id is part of this
|
|
266
302
|
* plugin's own bundle patch), so the translation layer sees the same explicit
|
|
267
303
|
* value the executor row carries instead of quietly resolving a different
|
|
268
304
|
* one. Absent row / absent config / any loader shape change reads ''.
|
|
305
|
+
*
|
|
306
|
+
* TWO access paths, because hosts differ (issue #13, measured on 0.2.0-rc.2):
|
|
307
|
+
* bundle-patched rows live in the OWNING PLUGIN'S SUBTREE there, so a bare
|
|
308
|
+
* `resolve('gitbash-executor')` throws from the root tree FOREVER (not just
|
|
309
|
+
* at boot) — the explicit tier silently read '' while the executor's own lazy
|
|
310
|
+
* `this.config.bashPath` worked, i.e. the two halves of this plugin disagreed
|
|
311
|
+
* about the user's explicit answer. `entries()` iterates subtrees as well, so
|
|
312
|
+
* it finds the row on both layouts; the direct resolve stays as the first try
|
|
313
|
+
* for older hosts where the row sits in the root tree.
|
|
269
314
|
* @param {object} ctx - a context exposing `loader`.
|
|
270
315
|
* @returns {string} the row's configured path, '' when unset.
|
|
271
316
|
*/
|
|
272
317
|
export function executorConfiguredBashPath(ctx) {
|
|
273
|
-
|
|
274
|
-
const entry = ctx && ctx.loader && typeof ctx.loader.resolve === 'function'
|
|
275
|
-
? ctx.loader.resolve('gitbash-executor')
|
|
276
|
-
: undefined
|
|
318
|
+
const read = (entry) => {
|
|
277
319
|
const value = entry && entry.options ? entry.options.config : undefined
|
|
278
320
|
return value && typeof value.bashPath === 'string' ? value.bashPath : ''
|
|
279
|
-
} catch {
|
|
280
|
-
return ''
|
|
281
321
|
}
|
|
322
|
+
const loader = ctx && ctx.loader
|
|
323
|
+
if (!loader) return ''
|
|
324
|
+
// Subtree walk: finds the row wherever the host nests bundle-patched rows.
|
|
325
|
+
const walk = () => {
|
|
326
|
+
try {
|
|
327
|
+
if (typeof loader.entries !== 'function') return undefined
|
|
328
|
+
for (const entry of loader.entries()) {
|
|
329
|
+
if (entry && entry.options && entry.options.id === 'gitbash-executor') return entry
|
|
330
|
+
}
|
|
331
|
+
return undefined
|
|
332
|
+
} catch {
|
|
333
|
+
return undefined
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
// Direct resolve first: the row sits in the ROOT tree on older hosts, and
|
|
338
|
+
// an EMPTY string it may carry is a real answer (= run the automatic
|
|
339
|
+
// chain) — only a throw (subtree row, newer hosts) falls through.
|
|
340
|
+
if (typeof loader.resolve === 'function') {
|
|
341
|
+
return read(loader.resolve('gitbash-executor'))
|
|
342
|
+
}
|
|
343
|
+
} catch { /* not in the root tree — walk the subtrees below */ }
|
|
344
|
+
return read(walk())
|
|
282
345
|
}
|
|
283
346
|
|
|
284
347
|
/**
|
|
@@ -289,19 +352,29 @@ export function executorConfiguredBashPath(ctx) {
|
|
|
289
352
|
* @returns {object} the production io.
|
|
290
353
|
*/
|
|
291
354
|
export function defaultIo() {
|
|
355
|
+
// spawnSync (not execFileSync): the sync-exec family LEAKS a failing
|
|
356
|
+
// child's stderr straight onto the host log (reg's GBK error text showed
|
|
357
|
+
// up as mojibake on every failed probe), while spawnSync with piped stdio
|
|
358
|
+
// captures it into the result where the catch below can drop it.
|
|
292
359
|
const run = (file, args) => {
|
|
293
360
|
try {
|
|
294
|
-
|
|
361
|
+
const result = spawnSync(file, args, { encoding: 'utf8', timeout: 5000, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
362
|
+
if (result.error !== undefined || result.status !== 0) return undefined
|
|
363
|
+
return String(result.stdout ?? '').trim()
|
|
295
364
|
} catch {
|
|
296
365
|
return undefined
|
|
297
366
|
}
|
|
298
367
|
}
|
|
368
|
+
const registryValue = (hive, key, name) => {
|
|
369
|
+
const out = run('reg', ['query', hive + '\\' + key, '/v', name])
|
|
370
|
+
if (typeof out !== 'string') return undefined
|
|
371
|
+
const match = new RegExp(name + '\\s+REG_(?:EXPAND_)?SZ\\s+(.*)', 'i').exec(out)
|
|
372
|
+
return match === null ? undefined : match[1].trim()
|
|
373
|
+
}
|
|
299
374
|
const registryPath = (hive, key) => {
|
|
300
|
-
const
|
|
301
|
-
if (typeof
|
|
302
|
-
|
|
303
|
-
if (match === null) return []
|
|
304
|
-
return match[1].split(';').map((entry) => entry.trim()).filter((entry) => entry !== '')
|
|
375
|
+
const value = registryValue(hive, key, 'Path')
|
|
376
|
+
if (typeof value !== 'string') return []
|
|
377
|
+
return value.split(';').map((entry) => entry.trim()).filter((entry) => entry !== '')
|
|
305
378
|
}
|
|
306
379
|
const dirs = () => {
|
|
307
380
|
const out = []
|
|
@@ -314,6 +387,23 @@ export function defaultIo() {
|
|
|
314
387
|
return out
|
|
315
388
|
}
|
|
316
389
|
const pathDirs = () => String(process.env.PATH ?? '').split(process.platform === 'win32' ? ';' : ':').map((entry) => entry.trim()).filter((entry) => entry !== '')
|
|
390
|
+
const gitForWindowsInstalls = () => {
|
|
391
|
+
const roots = []
|
|
392
|
+
// The official installer writes these (machine-wide / per-user); the
|
|
393
|
+
// 32-bit-on-64-bit install lives under WOW6432Node. Scoop/portable usually
|
|
394
|
+
// has none of them, which is why this rides behind `git --exec-path`.
|
|
395
|
+
for (const [hive, key] of [
|
|
396
|
+
['HKLM', 'SOFTWARE\\GitForWindows'],
|
|
397
|
+
['HKLM', 'SOFTWARE\\WOW6432Node\\GitForWindows'],
|
|
398
|
+
['HKCU', 'Software\\GitForWindows'],
|
|
399
|
+
]) {
|
|
400
|
+
const install = registryValue(hive, key, 'InstallPath')
|
|
401
|
+
if (typeof install === 'string' && install.trim() !== '') {
|
|
402
|
+
roots.push(install.replace(/\\/g, '/').replace(/\/+$/, '') + '/bin/bash.exe')
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return roots
|
|
406
|
+
}
|
|
317
407
|
return {
|
|
318
408
|
exists: (path) => {
|
|
319
409
|
try { return existsSync(path) } catch { return false }
|
|
@@ -326,6 +416,19 @@ export function defaultIo() {
|
|
|
326
416
|
const parent = dirname(dir.replace(/\\/g, '/'))
|
|
327
417
|
return [parent + '/bin/bash.exe', dirname(parent) + '/bin/bash.exe']
|
|
328
418
|
}),
|
|
419
|
+
// issue #13: shimmed Git layouts (Scoop, portable) have no bash.exe on PATH
|
|
420
|
+
// and a shim directory that defeats the cmd/bin reverse lookup. The REAL
|
|
421
|
+
// git reports its own home through --exec-path even when launched via a
|
|
422
|
+
// shim, and the official installer's InstallPath backs it up.
|
|
423
|
+
execPathBashCandidates: () => {
|
|
424
|
+
const gitExe = pathDirs()
|
|
425
|
+
.map((dir) => dir.replace(/\\/g, '/') + '/git.exe')
|
|
426
|
+
.find((path) => { try { return existsSync(path) } catch { return false } })
|
|
427
|
+
const execPath = gitExe === undefined ? undefined : run(gitExe, ['--exec-path'])
|
|
428
|
+
return [...execPathBashCandidates(execPath, (path) => {
|
|
429
|
+
try { return existsSync(path) } catch { return false }
|
|
430
|
+
}), ...gitForWindowsInstalls()]
|
|
431
|
+
},
|
|
329
432
|
registryBashCandidates: () => [
|
|
330
433
|
...registryPath('HKCU', 'Environment'),
|
|
331
434
|
...registryPath('HKLM', 'SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment'),
|
package/src/index.js
CHANGED
|
@@ -276,7 +276,7 @@ export function effectivePresetIds(configured, { suppress, peerGitBash } = {}) {
|
|
|
276
276
|
*/
|
|
277
277
|
import {
|
|
278
278
|
DEFAULT_GIT_BASH, GIT_BASH_DOWNLOAD_URL, bashResolutionReport,
|
|
279
|
-
effectiveConfiguredBashPath, executorConfiguredBashPath, resolveGitBashCached,
|
|
279
|
+
effectiveConfiguredBashPath, executorConfiguredBashPath, resolveGitBashCached,
|
|
280
280
|
} from './bash-path.js'
|
|
281
281
|
import { adoptTerminalShell, terminalAdoptReport } from './terminal-shell.js'
|
|
282
282
|
import {
|
|
@@ -481,6 +481,24 @@ export function valueOf(value) {
|
|
|
481
481
|
return value && typeof value.get === 'function' ? value.get() : value
|
|
482
482
|
}
|
|
483
483
|
|
|
484
|
+
/**
|
|
485
|
+
* This row's OWN live `bashPath` (issue #13): apply()'s `config` parameter IS
|
|
486
|
+
* the `gitbash-shell` row's resolved Config, so reading it here replaces the
|
|
487
|
+
* settings-service detour (`settingsBashPath`) that could read '' at apply
|
|
488
|
+
* time on hosts where the service is describe()/update()-only or not injected
|
|
489
|
+
* yet. Volatile refs mean a settings edit lands on the next read, no listener.
|
|
490
|
+
* @param {object|undefined} config - apply()'s resolved Config (may be absent).
|
|
491
|
+
* @returns {string} the row's `bashPath`, '' when unset or unreadable.
|
|
492
|
+
*/
|
|
493
|
+
export function rowBashPath(config) {
|
|
494
|
+
try {
|
|
495
|
+
const value = config && typeof config === 'object' ? valueOf(config.bashPath) : undefined
|
|
496
|
+
return typeof value === 'string' ? value.trim() : ''
|
|
497
|
+
} catch {
|
|
498
|
+
return ''
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
484
502
|
/**
|
|
485
503
|
* Era-aware live settings reader for everything wired inside apply(): the
|
|
486
504
|
* NEW era reads this plugin's own Config refs (no listener needed — every
|
|
@@ -1805,25 +1823,38 @@ export async function apply(ctx, config = {}) {
|
|
|
1805
1823
|
// materialize: service present, 'active: true' on Windows, 'active: false'
|
|
1806
1824
|
// where the bundle is installed but the platform stack stayed native.
|
|
1807
1825
|
/* The explicit tier of the resolution chain, in its documented priority:
|
|
1808
|
-
the `gitbash-executor` row's own config > the `gitbash-shell` row
|
|
1809
|
-
field > empty (= run the automatic chain). Both this layer and
|
|
1810
|
-
executor resolve from this same value, so an explicit answer is never
|
|
1826
|
+
the `gitbash-executor` row's own config > the `gitbash-shell` row's
|
|
1827
|
+
`bashPath` field > empty (= run the automatic chain). Both this layer and
|
|
1828
|
+
the executor resolve from this same value, so an explicit answer is never
|
|
1811
1829
|
substituted and never silently ignored.
|
|
1812
|
-
(issue #11: the row config is where the reporter's workaround landed.)
|
|
1813
|
-
|
|
1814
|
-
|
|
1830
|
+
(issue #11: the row config is where the reporter's workaround landed.)
|
|
1831
|
+
LAZY, never frozen (issue #13): at apply() time the loader store may not
|
|
1832
|
+
hold the executor row yet — `loader.resolve()` throws and reads '' — and
|
|
1833
|
+
the old settings-service detour could equally read '' before the service
|
|
1834
|
+
was injected. A frozen '' then ran the automatic chain (which fails on
|
|
1835
|
+
shimmed Git layouts) while the executor's own per-call lazy resolution
|
|
1836
|
+
worked, i.e. the same boot gave OPPOSITE verdicts. Every consumer below
|
|
1837
|
+
re-reads per use; the own row's value comes from apply()'s `config`
|
|
1838
|
+
parameter (this row's live Config refs — volatile, so a settings edit is
|
|
1839
|
+
visible on the next read without any listener). */
|
|
1840
|
+
const readConfiguredBashPath = () => effectiveConfiguredBashPath(executorConfiguredBashPath(ctx), rowBashPath(config))
|
|
1841
|
+
const readBashResolution = () => resolveGitBashCached({ configured: readConfiguredBashPath() })
|
|
1815
1842
|
const gitBashCapability = {
|
|
1816
1843
|
active: process.platform === 'win32',
|
|
1817
1844
|
// Kept for existing consumers (peer plugins read `bashPath`): the resolved
|
|
1818
1845
|
// interpreter. When resolution failed we report the explicit value (or the
|
|
1819
1846
|
// historical default) so the ENOENT a consumer may hit names a real path —
|
|
1820
|
-
// never a substitute shell.
|
|
1821
|
-
|
|
1847
|
+
// never a substitute shell. Getters keep the capability live (issue #13):
|
|
1848
|
+
// a consumer reading it after the rows settle sees the settled verdict.
|
|
1849
|
+
get bashPath() {
|
|
1850
|
+
const resolution = readBashResolution()
|
|
1851
|
+
return resolution.ok ? resolution.path : (readConfiguredBashPath() || DEFAULT_GIT_BASH)
|
|
1852
|
+
},
|
|
1822
1853
|
// Additive (v0.28.0): the full verdict, for the guided popup and peers.
|
|
1823
|
-
ok
|
|
1824
|
-
source
|
|
1825
|
-
configured
|
|
1826
|
-
tried
|
|
1854
|
+
get ok() { return readBashResolution().ok },
|
|
1855
|
+
get source() { return readBashResolution().source },
|
|
1856
|
+
get configured() { return readBashResolution().configured },
|
|
1857
|
+
get tried() { return readBashResolution().tried },
|
|
1827
1858
|
downloadUrl: GIT_BASH_DOWNLOAD_URL,
|
|
1828
1859
|
}
|
|
1829
1860
|
const disposeGitBash = ctx.provide('gitBash', gitBashCapability)
|
|
@@ -1979,7 +2010,7 @@ export async function apply(ctx, config = {}) {
|
|
|
1979
2010
|
// untouched and the result metadata is not echoed back in MSYS form.
|
|
1980
2011
|
if (dialect.posixPaths && dialectApplies(dialect, exec && exec.agent)) {
|
|
1981
2012
|
echo = true
|
|
1982
|
-
const env = dialect.virtualMounts ? buildTranslateEnv(
|
|
2013
|
+
const env = dialect.virtualMounts ? buildTranslateEnv(readConfiguredBashPath()) : null
|
|
1983
2014
|
echoEnv = env
|
|
1984
2015
|
if (exec && exec.arguments && typeof exec.arguments === 'object') {
|
|
1985
2016
|
const translated = translateDispatch(exec, dialect, env)
|
|
@@ -2034,7 +2065,7 @@ export async function apply(ctx, config = {}) {
|
|
|
2034
2065
|
if (!dialectApplies(dialect, exec && exec.agent)) return next()
|
|
2035
2066
|
if (dialect.posixPaths && dialect.errorDialect && exec && result && typeof result === 'object'
|
|
2036
2067
|
&& result.isError === true && Array.isArray(result.content)) {
|
|
2037
|
-
const env = dialect.virtualMounts ? buildTranslateEnv(
|
|
2068
|
+
const env = dialect.virtualMounts ? buildTranslateEnv(readConfiguredBashPath()) : null
|
|
2038
2069
|
if (exec.name === 'run_code') {
|
|
2039
2070
|
// paths only — the /dev/null hint is for file tools
|
|
2040
2071
|
const content = rewriteErrorContent(result.content, { nulHint: false, env })
|
|
@@ -2108,14 +2139,59 @@ export async function apply(ctx, config = {}) {
|
|
|
2108
2139
|
// Now the boot log names the value, the whole probe chain and where to fix
|
|
2109
2140
|
// it, and the client half reads the same verdict over HTTP to raise the
|
|
2110
2141
|
// guided popup. Nothing here substitutes another shell — by design.
|
|
2111
|
-
|
|
2142
|
+
//
|
|
2143
|
+
// issue #13: the boot verdict may legitimately be wrong for a few seconds —
|
|
2144
|
+
// at apply() time the loader store may not hold the executor row yet, so
|
|
2145
|
+
// the explicit tier reads '' while the executor's own per-call lazy
|
|
2146
|
+
// resolution succeeds. A FAILURE with an EMPTY configured value is therefore
|
|
2147
|
+
// not final: poll briefly for the explicit tier to appear as the rows
|
|
2148
|
+
// settle, print the settled verdict when it changes, and notify the
|
|
2149
|
+
// adoptions below (official terminal / better-sidebar) through
|
|
2150
|
+
// `onBashSettled`. A failure with a NON-empty configured value IS final (the
|
|
2151
|
+
// user's explicit answer was rejected), and so is any success.
|
|
2152
|
+
let settledBashResolution = null
|
|
2153
|
+
const bashSettleWaiters = []
|
|
2154
|
+
const onBashSettled = (listener) => {
|
|
2155
|
+
if (settledBashResolution !== null) listener(settledBashResolution)
|
|
2156
|
+
else bashSettleWaiters.push(listener)
|
|
2157
|
+
}
|
|
2158
|
+
const settleBashResolution = (resolution) => {
|
|
2159
|
+
settledBashResolution = resolution
|
|
2160
|
+
for (const listener of bashSettleWaiters.splice(0)) {
|
|
2161
|
+
try { listener(resolution) } catch { /* a waiter never blocks settling */ }
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
const printBashResolution = (phase, resolution) => {
|
|
2165
|
+
console.log(TAG + ' bash resolution' + (phase === '' ? '' : ' (' + phase + ')') + ': '
|
|
2166
|
+
+ bashResolutionReport(resolution).split('\n').join('\n' + TAG + ' '))
|
|
2167
|
+
}
|
|
2168
|
+
const bootBashResolution = readBashResolution()
|
|
2169
|
+
printBashResolution('boot', bootBashResolution)
|
|
2170
|
+
// Non-win32 hosts settle immediately: resolution failure there is the
|
|
2171
|
+
// designed state (nothing consumes the capability), so no poll.
|
|
2172
|
+
if (bootBashResolution.ok || bootBashResolution.configured !== '' || process.platform !== 'win32') {
|
|
2173
|
+
settleBashResolution(bootBashResolution)
|
|
2174
|
+
} else {
|
|
2175
|
+
let ticks = 0
|
|
2176
|
+
const timer = setInterval(() => {
|
|
2177
|
+
ticks += 1
|
|
2178
|
+
if (readConfiguredBashPath() === '' && ticks < 20) return // rows still mounting
|
|
2179
|
+
clearInterval(timer)
|
|
2180
|
+
const settled = readBashResolution()
|
|
2181
|
+
if (settled.ok || settled.configured !== '') printBashResolution('settled', settled)
|
|
2182
|
+
settleBashResolution(settled)
|
|
2183
|
+
}, 250)
|
|
2184
|
+
ctx.effect(() => () => clearInterval(timer), 'dsh-gitbash-shell: bash resolution settle poll')
|
|
2185
|
+
}
|
|
2112
2186
|
if (process.platform === 'win32') {
|
|
2113
2187
|
try {
|
|
2114
2188
|
ctx.inject(['webServer'], (wctx) => {
|
|
2115
2189
|
try {
|
|
2116
2190
|
const route = '/dsh-gitbash-shell/api/status'
|
|
2117
2191
|
const read = () => {
|
|
2118
|
-
|
|
2192
|
+
// LIVE re-read (issue #13): the popup must see what the executor
|
|
2193
|
+
// actually resolves NOW, not a value frozen at apply() time.
|
|
2194
|
+
const resolution = resolveGitBashCached({ configured: readConfiguredBashPath(), fresh: true })
|
|
2119
2195
|
return {
|
|
2120
2196
|
ok: resolution.ok,
|
|
2121
2197
|
platform: process.platform,
|
|
@@ -2178,7 +2254,7 @@ export async function apply(ctx, config = {}) {
|
|
|
2178
2254
|
send(res, 403, { error: 'forbidden', reason: fence.reason })
|
|
2179
2255
|
return
|
|
2180
2256
|
}
|
|
2181
|
-
const env = buildTranslateEnv(
|
|
2257
|
+
const env = buildTranslateEnv(readConfiguredBashPath())
|
|
2182
2258
|
// One line per read: the browser half fetches this once per page load,
|
|
2183
2259
|
// so its presence in the log is how a user can tell the rescue is live.
|
|
2184
2260
|
console.log(TAG + ' path map read by ' + (req.socket && req.socket.remoteAddress ? req.socket.remoteAddress : '?'))
|
|
@@ -2304,10 +2380,14 @@ export async function apply(ctx, config = {}) {
|
|
|
2304
2380
|
attempts += 1
|
|
2305
2381
|
let result
|
|
2306
2382
|
try {
|
|
2383
|
+
// LAZY re-read per attempt (issue #13): the first try may run
|
|
2384
|
+
// before the loader store holds the executor row, when the
|
|
2385
|
+
// resolution legitimately still reads ''.
|
|
2386
|
+
const resolution = readBashResolution()
|
|
2307
2387
|
result = await adoptTerminalShell(editorCtx.configEditor, {
|
|
2308
2388
|
platform: process.platform,
|
|
2309
2389
|
enabled: liveSettings.autoTerminalShell(),
|
|
2310
|
-
resolved:
|
|
2390
|
+
resolved: resolution.ok ? resolution.path : '',
|
|
2311
2391
|
})
|
|
2312
2392
|
} catch (error) {
|
|
2313
2393
|
// adoptTerminalShell reports expected conditions itself; reaching
|
|
@@ -2332,9 +2412,20 @@ export async function apply(ctx, config = {}) {
|
|
|
2332
2412
|
// The sidebar resolves its terminal shell through the settings seam per
|
|
2333
2413
|
// open; adopt it through that seam (see adoptSidebarShell for rationale).
|
|
2334
2414
|
// Disable with `betterSidebarShell: false` in the plugin row config.
|
|
2415
|
+
// Rides the settle notification (issue #13): a boot-time failure with an
|
|
2416
|
+
// empty explicit tier may still settle into a success once the loader rows
|
|
2417
|
+
// mount, and the adoption then happens without a restart.
|
|
2335
2418
|
if (config.betterSidebarShell !== false && process.platform === 'win32') {
|
|
2336
|
-
|
|
2337
|
-
|
|
2419
|
+
let sidebarAdopted = false
|
|
2420
|
+
onBashSettled((resolution) => {
|
|
2421
|
+
if (resolution.ok) {
|
|
2422
|
+
if (sidebarAdopted) return
|
|
2423
|
+
sidebarAdopted = true
|
|
2424
|
+
adoptSidebarShell(ctx, resolution.path, () => liveSettings.adoptSidebar())
|
|
2425
|
+
return
|
|
2426
|
+
}
|
|
2427
|
+
console.log(TAG + ' sidebar terminal adoption skipped: no Git Bash resolved (nothing is substituted)')
|
|
2428
|
+
})
|
|
2338
2429
|
}
|
|
2339
2430
|
|
|
2340
2431
|
// ── era split: declarative registration on dsh >= 0.1.7 ──────────────────
|