dsh-gitbash-shell 0.30.0 → 0.31.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 CHANGED
@@ -3,6 +3,144 @@
3
3
  > 倒序排列,新版本条目在最上面。条目格式:`## vX.Y.Z — YYYY-MM-DD` + 类型(feat / fix / docs / chore)+ 要点 + 相关链接。
4
4
  > 纪律见 AGENTS.md「变更记录纪律」:发版前先更新本文件并随版本提交;事故复盘、复现与真机验证记录也记在这里。
5
5
 
6
+ ## v0.31.0 — 2026-09-27
7
+
8
+ **类型**:fix + feat(用户报告:点开对话里的文件链接,原生右侧栏显示「文件不存在,可能已被移动或删除」)
9
+
10
+ > 用户原话:**"dsh原生的右侧边栏不支持这种/c/ 路径? 让我们的gitbash插件能不能修复一下这个bug?"**
11
+ > 截图里 tab 标题是 `/c/Users/kanna/sandbox/dsh-remote-app/AGENTS.md`,内容区是那句 not-found。
12
+
13
+ ### 结论:不是 dsh 的 bug,是本插件引入的方言在 UI 侧没有闭环
14
+
15
+ dsh **设计上只有两种路径拼写**(POSIX 与 Windows 盘符/UNC,见 `dsh-util-workspace-path` 的
16
+ `isAbsoluteWorkspacePath`);MSYS 盘根是本插件教会模型与工具的东西,**官方核心不认识它**。
17
+ 修的责任因此在本插件这边 —— 而且这是本插件第一次把方言边界延伸到**浏览器 UI**。
18
+
19
+ ### 根因链(逐层定位,每一层都有代码位置)
20
+
21
+ 1. 右侧栏是官方包 `ui-sidebar-documentpreview`,它的失败文案按错误码 `workspace-file/not-found`
22
+ 映射(locales.ts:27,与截图逐字一致)。
23
+ 2. 读文件的宿主实现在 `packages/api/workspace-files/src/index.ts`:所有路径都经
24
+ `this.ctx.fs.resolve(path, { cwd: workspaceRoot })` / `fs.lstat(path, { cwd })`。
25
+ 3. `packages/fs/fs-local/src/fsio.ts:148-150` 在 win32 上就是 **`node:path.resolve(cwd, path)`**。
26
+ 4. `/c/Users/...` **不是** node 眼中的绝对路径(`isAbsolute` 只认 `C:\`、`\\server`、posix 根),
27
+ 于是它被解析成 **`<当前盘符>:\c\Users\...`** —— 一个不存在的路径 ⇒ not-found。
28
+ 5. 那个 `/c/...` 是从哪来的:对话里的文件链接文本来自 **shell 输出**,而本插件让 shell 说 MSYS 方言。
29
+ 链接打开走 `ui-chat` 的 `openFile` → `fileAddressFor(sessionId, cwd, path)`
30
+ (`packages/util/workspace-path`):它用 `cwd` 前缀判断"工作区内→转相对路径"。本机实测 session 目录名是
31
+ `--C-Users-kanna-sandbox--`,即 **cwd 是 Windows 拼写**(`C:/Users/kanna/sandbox`),而链接文本是
32
+ `/c/Users/kanna/sandbox/...` —— 前缀不匹配 ⇒ 原样保留绝对 MSYS 拼写 ⇒ 第 4 步必然失败。
33
+
34
+ ### 改动
35
+
36
+ **宿主半(`src/index.js`)**:新增只读路由 `GET /dsh-gitbash-shell/api/pathmap`,返回
37
+ `{ platform, home, tmpDir, gitRoot, mounts }` —— 就是既有翻译层 `buildTranslateEnv()` 的同一份事实
38
+ (挂载表直接从 `GIT_MOUNTS` 生成,不另抄一份),加 `process.platform` 供客户端判断。与工具链路由同款
39
+ loopback + same-origin fence(`fenceToolRequest`;GET 只需 loopback),因为 web 服务器可能绑在 `0.0.0.0`,
40
+ 而这个路由会报出账号自己的目录。
41
+
42
+ **客户端半(`src/client.js`)**:包装 `ctx.sidebarRight` 的 `openResource` 与 `openResourceIn` ——
43
+ 官方文档写明**"每一种进入右侧栏的方式都是这两者之一的调用"**(对话文件链接、工具行行号引用、
44
+ 文件树的每一行),所以一个包装点覆盖全部入口,**不碰任何 dsh 源码**。
45
+
46
+ - 地址语法按官方 `file-address.ts` 实现:解析 `dsh-resource://file/{session/<id>|<absolute>}/<path>`
47
+ (逐段 `encodeURIComponent`、`:` 保留、UNC 空首段),改写后按同一编码规则重建,`?`/`#` 后缀原样保留。
48
+ - **未改变即原样转发原字符串**(不是"重建后看起来一样"),所以没有翻译过的地址绝不会被 URL 往返改动。
49
+ - 转换语义与宿主 `translateMsysPath` 逐例对齐(盘根 → `X:/`、`~` → home、`/tmp` → 用户 TEMP、
50
+ `/dev/null` → `\\.\NUL`、`/usr` 系 → Git 根);**不实现宿主的 `$VAR` 展开** —— 对话里的路径是文本不是 shell 词。
51
+ - **门是宿主的事实,不是猜测**:`pathRescueActive()` 要求 `/api/pathmap` 已应答且 `platform === 'win32'`。
52
+ macOS/Linux 宿主**永不**翻译(客户端的 user agent 说明不了路径属于谁的文件系统);fact 未到时也不翻译
53
+ (退化为打补丁前的行为,而不是赌一把)。
54
+ - **可选注入**:`ctx.inject(["sidebarRight"], …)` 而非写进 `exports.inject` —— 硬注入一个本插件不拥有的服务
55
+ 会让 fiber 在缺该服务的宿主上永久 PENDING(v0.24.0 的 settingsScope 教训)。
56
+ - 包装挂在服务实例上,经 `sctx.effect` 随插件 fiber 卸载并还原原方法。
57
+
58
+ ### 验证
59
+
60
+ - `npm test` **118/118 全绿**(新增 6 项):
61
+ - 驱动**真实 bundle**(fake cordis ctx + fake `sidebarRight` 记录收到的地址)覆盖地址语法:
62
+ 盘根 / `~` / `/tmp` / `/usr` / `/etc`、absolute scope、`?line=12` 后缀、UNC 原样、非 file 地址原样、
63
+ 非法转义原样、边界不误伤(`/tmpfoo`、`/usrx`、`/Tmp`、`/ab/c`)、已是 Windows 拼写原样、相对路径原样。
64
+ - **宿主事实门**:`platform: 'linux'` 与"fetch 未应答"两种情况下地址逐字节原样。
65
+ - **可逆性**:disposer 还原原方法;`exports.inject` 不得含 `sidebarRight`(静态断言)。
66
+ - **浏览器镜像与宿主翻译器逐例比对**:23 个输入跑 `_internal.translateMsysPath` 与客户端实现,结果必须相同
67
+ —— 锁死两边语义漂移。
68
+ - **真实文件系统实测(win32)**:用本仓库 `package.json` 造 `/c/...` 形式,断言
69
+ `path.resolve(cwd, msys)` 指向**不存在**的路径(这就是用户看到的 not-found),而救回后的
70
+ `C:/...` **existsSync 为真** —— bug 与修复都在真文件上量了一遍。
71
+ - **路由实测**:驱动真实 `apply()`,断言路由注册为 `prefix`、loopback GET 返回 200 且
72
+ `platform` 等于 `process.platform`、mounts 键序与客户端遍历序一致、非 loopback 返回 403 `not-loopback`。
73
+ - **真机验证(隔离实例,未碰用户正在运行的实例)**:把用户 web profile 复制到一个独立 `DSH_HOME`
74
+ (Windows 上 `cp -rL` 解引用 pnpm 链接,97M → 366M,5 秒),把插件目录换成**本仓源码**,独立端口起
75
+ `dsh web`,然后:
76
+ - 宿主侧:`GET /dsh-gitbash-shell/api/pathmap` 实测 **HTTP 200**,返回
77
+ `{"platform":"win32","home":"C:/Users/kanna","tmpDir":"C:/Users/kanna/AppData/Local/Temp",
78
+ "gitRoot":"c:/program files/git","mounts":{…7 项,键序与客户端遍历序一致}}`
79
+ —— 启动日志出现 `MSYS path map route active at /dsh-gitbash-shell/api/pathmap`。
80
+ - 客户端侧:首页 `window.__DSH_BOOT__` 里 `dsh-gitbash-shell` 出现 5 次(启动图含本插件);
81
+ `curl` 它实际会取的 combo URL(`/plugins/??dsh-gitbash-shell/client.js&rev=…`)返回 **200**,
82
+ 内容含完整新代码段且**语法有效**。
83
+ - **真实浏览器(CDP 驱动 driver 自己的隔离 Chromium,不动用户的浏览器)控制台实测出现**
84
+ `[gitbash-shell/client] right-sidebar path rescue armed (host spelling restored on open)`
85
+ —— 这一行在 `ctx.inject(["sidebarRight"], …)` 的回调内部,所以它同时证明:client 半在真实浏览器
86
+ 里 apply、**`sidebarRight` 客户端服务可见、inject 回调被调用、`openResource` 是函数且已被包装**。
87
+ - 宿主日志随后出现 `path map read by <loopback>` —— **浏览器真的拉到了那份宿主事实**,
88
+ 即 `pathRescueActive()` 在生产链路里会返回 true。
89
+ - 路由新增一行访问日志(`path map read by <addr>`),既是诊断手段,也是上面这条验证的观测点。
90
+ - **仍未覆盖**:真实 UI 里"点击 `/c/...` 链接 → tab 显示出文件内容"这最后一跳(需要先构造一个带
91
+ MSYS 路径的会话)。这一跳的两端都被验证过 —— 地址重写(单元矩阵)与宿主可读性(真实文件
92
+ `existsSync`);用户在重启客户端后即可实测确认。
93
+
94
+ ### 已知边界(如实记录)
95
+
96
+ - 只有"整体就是一条路径"的链接能被救:`/c/Users/x` 成立;而 shell 输出里**被文本截断/拼接**的路径、
97
+ 以及 `/c` 之外的 MSYS 虚拟根(若宿主探测不到 Git 根,tmpDir/gitRoot 为 null)保持原样 ——
98
+ 退化行为与修复前一致,**不会**产生错误路径。
99
+ - `/dev/null` 这类设备路径即使被点开也没有可读内容(与文件工具一致,EINVAL 如实报错)。
100
+
101
+ ## v0.30.1 — 2026-09-26
102
+
103
+ **类型**:fix(用户实测反馈 v0.30.0 的工具列表:点了按钮看不到进度、看不到在装哪一个、看不到报错、还能连点)
104
+
105
+ > 用户原话:**"我这点击升级了都不知道进度,有没有在升级,也看不到报错,你优化一下,可能看到目前按安装还是更新那个,进度条什么的放在那个列表里。然后不让我连点,开始了就不用再点。"**
106
+
107
+ ### 根因(两个都是真缺陷,不是用法问题)
108
+
109
+ 1. **busy 的生命周期错了**:`runTools()` 在 POST 收到 202 时就 `setToolBusy(false)`,而任务在**宿主侧继续跑**
110
+ —— 于是按钮在批次刚开始时就恢复可点。这正是"能连点"的来源:后端的单任务闸门确实会返回 409,
111
+ 但用户看到的是一个**亮着的按钮**,自然会再点。
112
+ 2. **进度放在了一个没人看的地方**:进度只渲染在按钮下方的一块区域,**不在工具列表行内**;
113
+ 每行也不显示自己的状态,失败原因混在一个游离的结果列表里,既不显眼也不对应行。
114
+
115
+ ### 改动(`src/client.js`)
116
+
117
+ - **busy 从「请求在飞」改为「任务在跑」**:新增派生状态 `jobRunning = toolJob && toolJob.finished !== true`
118
+ 与 `busyNow = toolBusy || jobRunning`;安装 / 刷新 / 全选三个按钮与**所有复选框**在整个批次期间禁用,
119
+ 主按钮文案变为「处理中」。开始之后不必也**不能**再点。
120
+ - **进度搬进列表行**:正在处理的那一行高亮,行内依次显示 spinner + **「安装中…」/「更新中…」**
121
+ (按该行 `state` 判断是装还是升,不靠猜)+ 一条扫动进度条;已完成的行保留 ✓/✗ 判定。
122
+ - **错误就地可见**:失败的行**正下方**用红字打印「安装中 — 失败: <winget 原文>」,不用再去别处找日志。
123
+ - **整体进度**:任务运行时显示 spinner + `当前工具 · 处理中: n / m` + 按完成数计算的进度条(宽度随 done/total 增长)。
124
+ - 移除原先那个游离的结果列表 —— 信息已全部进入对应行。
125
+ - 新增样式:行高亮、运行态徽章、行内扫动条、整体进度条、失败行、spinner,以及 `gb-spin` / `gb-slide` 两条 keyframes。
126
+ - 词典 **21 语言各补 2 键**(`tools.installing` / `tools.updating`),现共 **65 键**,键对齐由冒烟强制。
127
+
128
+ ### 验证
129
+
130
+ - `npm test` **112/112 全绿**(词典键对齐断言覆盖新键)。
131
+ - 静态校验:65 个词典键**全部**被代码引用,零悬空引用。
132
+ - ⚠️ 仍未做:浏览器端真机点击验证(需重启客户端后自测)。
133
+ - 版本号 `0.30.0` → **`0.30.1`**。
134
+
135
+ ### 附:一次误操作与回滚(如实记录)
136
+
137
+ 本次最初**误把第三方插件 `dshmarket` 当成了目标**,对
138
+ `~/.dsh/profiles/desktop/node_modules/dshmarket/client/client.js` 打了一处本地补丁(让自我更新时
139
+ dshmarket 自己出现在「已安装」列表里,以显示行内进度)。用户澄清后**已完整回滚**:
140
+ 与 npm 上的 `dshmarket@1.66.2` 逐字节 `cmp` 一致,零残留。
141
+ (该补丁对应的确实是一个真实缺陷 —— 自我更新按钮 `onClick` 会 `setTab("installed")` 想看进度,
142
+ 但列表又 `filter` 掉了 `selfName` —— 留作后续,未在本仓库处理。)
143
+
6
144
  ## v0.30.0 — 2026-09-26
7
145
 
8
146
  **类型**:feat(新能力 —— 设置卡里一键安装/升级一份**固定的终端工具清单**,全部经 winget 官方源)
package/README.md CHANGED
@@ -316,7 +316,14 @@ PTC/创造及用户自建模式一律覆盖。
316
316
  用户 TEMP 下的路径回显为 `/tmp/...`(与 bash 的 `$TMP` 一致);文件内容与错误结果不动;
317
317
  `grep` 的 `matches[].path`)改写回 MSYS 形式;文件内容与错误结果不动;
318
318
  - **运行时事实**:向官方 `dsh-shell-env` 注册表贡献 `DSH_PATH_DIALECT=msys`,模型可在执行时
319
- 核验(随开关实时生效)。
319
+ 核验(随开关实时生效);
320
+ - **右侧栏文件链接(v0.31.0)**:对话里的文件链接带着 `/c/...` 拼写进入 dsh **原生右侧栏**
321
+ (`ui-sidebar-documentpreview`),而宿主文件系统只认 Windows 盘符/UNC 拼写 —— `/c/...` 会被
322
+ 解析成 `<当前盘符>:\c\...`,于是显示「文件不存在,可能已被移动或删除」。插件在右侧栏的**唯一
323
+ 入口**(`ctx.sidebarRight.openResource`)上把地址还原成宿主拼写,`~` / `/tmp` / `/dev/null` /
324
+ `/usr` 等挂载点按与工具参数翻译**同一张表**处理;宿主事实经只读路由
325
+ `GET /dsh-gitbash-shell/api/pathmap` 提供,**只有宿主自报 `win32` 时才改写**
326
+ (macOS/Linux 宿主永不翻译,事实未到时也不翻译)。**没有改动任何 dsh 源码**,包装随插件卸载还原。
320
327
 
321
328
  关闭后回到 dsh 原生行为:指示文本为空(组装期直接丢弃,零提示噪声),路径参数与结果元数据
322
329
  都不再改写,文件工具收 Windows 路径。**bash 始终是 Git Bash,不受此开关影响**——它只决定
package/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-external/dsh-gitbash-shell",
3
- "version": "0.30.0",
3
+ "version": "0.31.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。双 era 适配 dsh <= 0.1.1(内置 id 'code')与 >= 0.1.2(改名 'ptc'),启动探测自动选择,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.30.0",
3
+ "version": "0.31.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/client.js CHANGED
@@ -131,6 +131,8 @@ window.__ModuleLoader__.load({
131
131
  "tools.working": "处理中",
132
132
  "tools.resultOk": "成功",
133
133
  "tools.resultFail": "失败",
134
+ "tools.installing": "安装中…",
135
+ "tools.updating": "更新中…",
134
136
  };
135
137
 
136
138
  var en = {
@@ -197,6 +199,8 @@ window.__ModuleLoader__.load({
197
199
  "tools.working": "Working",
198
200
  "tools.resultOk": "succeeded",
199
201
  "tools.resultFail": "failed",
202
+ "tools.installing": "Installing…",
203
+ "tools.updating": "Updating…",
200
204
  };
201
205
 
202
206
  /* Third-language dictionaries: one entry per language, each preceded
@@ -273,6 +277,8 @@ window.__ModuleLoader__.load({
273
277
  "tools.working": "جارٍ العمل",
274
278
  "tools.resultOk": "نجح",
275
279
  "tools.resultFail": "فشل",
280
+ "tools.installing": "جارٍ التثبيت…",
281
+ "tools.updating": "جارٍ الترقية…",
276
282
  },
277
283
  /* locale: de */
278
284
  "de": {
@@ -339,6 +345,8 @@ window.__ModuleLoader__.load({
339
345
  "tools.working": "Läuft",
340
346
  "tools.resultOk": "erfolgreich",
341
347
  "tools.resultFail": "fehlgeschlagen",
348
+ "tools.installing": "Wird installiert…",
349
+ "tools.updating": "Wird aktualisiert…",
342
350
  },
343
351
  /* locale: fr */
344
352
  "fr": {
@@ -405,6 +413,8 @@ window.__ModuleLoader__.load({
405
413
  "tools.working": "En cours",
406
414
  "tools.resultOk": "réussi",
407
415
  "tools.resultFail": "échec",
416
+ "tools.installing": "Installation…",
417
+ "tools.updating": "Mise à jour…",
408
418
  },
409
419
  /* locale: hi */
410
420
  "hi": {
@@ -471,6 +481,8 @@ window.__ModuleLoader__.load({
471
481
  "tools.working": "चल रहा है",
472
482
  "tools.resultOk": "सफल",
473
483
  "tools.resultFail": "विफल",
484
+ "tools.installing": "इंस्टॉल हो रहा है…",
485
+ "tools.updating": "अपडेट हो रहा है…",
474
486
  },
475
487
  /* locale: id */
476
488
  "id": {
@@ -537,6 +549,8 @@ window.__ModuleLoader__.load({
537
549
  "tools.working": "Memproses",
538
550
  "tools.resultOk": "berhasil",
539
551
  "tools.resultFail": "gagal",
552
+ "tools.installing": "Memasang…",
553
+ "tools.updating": "Memperbarui…",
540
554
  },
541
555
  /* locale: it */
542
556
  "it": {
@@ -603,6 +617,8 @@ window.__ModuleLoader__.load({
603
617
  "tools.working": "In corso",
604
618
  "tools.resultOk": "riuscito",
605
619
  "tools.resultFail": "non riuscito",
620
+ "tools.installing": "Installazione…",
621
+ "tools.updating": "Aggiornamento…",
606
622
  },
607
623
  /* locale: ja */
608
624
  "ja": {
@@ -669,6 +685,8 @@ window.__ModuleLoader__.load({
669
685
  "tools.working": "処理中",
670
686
  "tools.resultOk": "成功",
671
687
  "tools.resultFail": "失敗",
688
+ "tools.installing": "インストール中…",
689
+ "tools.updating": "更新中…",
672
690
  },
673
691
  /* locale: ko */
674
692
  "ko": {
@@ -735,6 +753,8 @@ window.__ModuleLoader__.load({
735
753
  "tools.working": "처리 중",
736
754
  "tools.resultOk": "성공",
737
755
  "tools.resultFail": "실패",
756
+ "tools.installing": "설치 중…",
757
+ "tools.updating": "업데이트 중…",
738
758
  },
739
759
  /* locale: nl */
740
760
  "nl": {
@@ -801,6 +821,8 @@ window.__ModuleLoader__.load({
801
821
  "tools.working": "Bezig",
802
822
  "tools.resultOk": "geslaagd",
803
823
  "tools.resultFail": "mislukt",
824
+ "tools.installing": "Installeren…",
825
+ "tools.updating": "Bijwerken…",
804
826
  },
805
827
  /* locale: pl */
806
828
  "pl": {
@@ -867,6 +889,8 @@ window.__ModuleLoader__.load({
867
889
  "tools.working": "Przetwarzanie",
868
890
  "tools.resultOk": "sukces",
869
891
  "tools.resultFail": "niepowodzenie",
892
+ "tools.installing": "Instalowanie…",
893
+ "tools.updating": "Aktualizowanie…",
870
894
  },
871
895
  /* locale: pt */
872
896
  "pt": {
@@ -933,6 +957,8 @@ window.__ModuleLoader__.load({
933
957
  "tools.working": "A processar",
934
958
  "tools.resultOk": "com êxito",
935
959
  "tools.resultFail": "falhou",
960
+ "tools.installing": "A instalar…",
961
+ "tools.updating": "A atualizar…",
936
962
  },
937
963
  /* locale: ru */
938
964
  "ru": {
@@ -999,6 +1025,8 @@ window.__ModuleLoader__.load({
999
1025
  "tools.working": "Выполняется",
1000
1026
  "tools.resultOk": "успешно",
1001
1027
  "tools.resultFail": "ошибка",
1028
+ "tools.installing": "Установка…",
1029
+ "tools.updating": "Обновление…",
1002
1030
  },
1003
1031
  /* locale: sv */
1004
1032
  "sv": {
@@ -1065,6 +1093,8 @@ window.__ModuleLoader__.load({
1065
1093
  "tools.working": "Arbetar",
1066
1094
  "tools.resultOk": "lyckades",
1067
1095
  "tools.resultFail": "misslyckades",
1096
+ "tools.installing": "Installerar…",
1097
+ "tools.updating": "Uppdaterar…",
1068
1098
  },
1069
1099
  /* locale: th */
1070
1100
  "th": {
@@ -1131,6 +1161,8 @@ window.__ModuleLoader__.load({
1131
1161
  "tools.working": "กำลังดำเนินการ",
1132
1162
  "tools.resultOk": "สำเร็จ",
1133
1163
  "tools.resultFail": "ล้มเหลว",
1164
+ "tools.installing": "กำลังติดตั้ง…",
1165
+ "tools.updating": "กำลังอัปเกรด…",
1134
1166
  },
1135
1167
  /* locale: tr */
1136
1168
  "tr": {
@@ -1197,6 +1229,8 @@ window.__ModuleLoader__.load({
1197
1229
  "tools.working": "İşleniyor",
1198
1230
  "tools.resultOk": "başarılı",
1199
1231
  "tools.resultFail": "başarısız",
1232
+ "tools.installing": "Kuruluyor…",
1233
+ "tools.updating": "Güncelleniyor…",
1200
1234
  },
1201
1235
  /* locale: vi */
1202
1236
  "vi": {
@@ -1263,6 +1297,8 @@ window.__ModuleLoader__.load({
1263
1297
  "tools.working": "Đang xử lý",
1264
1298
  "tools.resultOk": "thành công",
1265
1299
  "tools.resultFail": "thất bại",
1300
+ "tools.installing": "Đang cài…",
1301
+ "tools.updating": "Đang nâng cấp…",
1266
1302
  },
1267
1303
  /* locale: zh-hk */
1268
1304
  "zh-hk": {
@@ -1329,6 +1365,8 @@ window.__ModuleLoader__.load({
1329
1365
  "tools.working": "處理緊",
1330
1366
  "tools.resultOk": "成功",
1331
1367
  "tools.resultFail": "失敗",
1368
+ "tools.installing": "安裝緊…",
1369
+ "tools.updating": "升級緊…",
1332
1370
  },
1333
1371
  /* locale: zh-mo */
1334
1372
  "zh-mo": {
@@ -1395,6 +1433,8 @@ window.__ModuleLoader__.load({
1395
1433
  "tools.working": "處理緊",
1396
1434
  "tools.resultOk": "成功",
1397
1435
  "tools.resultFail": "失敗",
1436
+ "tools.installing": "安裝緊…",
1437
+ "tools.updating": "升級緊…",
1398
1438
  },
1399
1439
  /* locale: zh-tw */
1400
1440
  "zh-tw": {
@@ -1461,6 +1501,8 @@ window.__ModuleLoader__.load({
1461
1501
  "tools.working": "處理中",
1462
1502
  "tools.resultOk": "成功",
1463
1503
  "tools.resultFail": "失敗",
1504
+ "tools.installing": "安裝中…",
1505
+ "tools.updating": "更新中…",
1464
1506
  },
1465
1507
  };
1466
1508
 
@@ -1581,6 +1623,21 @@ window.__ModuleLoader__.load({
1581
1623
  ".gb-ok{flex:none;color:var(--dsw-alias-state-success-primary,#3fb950)}",
1582
1624
  ".gb-bad{flex:none;color:var(--dsw-alias-status-danger,#e5484d)}",
1583
1625
  ".gb-segBtn:disabled{opacity:.45;cursor:default}",
1626
+ /* in-list progress (v0.30.1): the row being installed/upgraded shows a
1627
+ spinner, its action, and a sweeping bar; finished rows keep their
1628
+ verdict, and a failure prints its reason under the row. */
1629
+ ".gb-toolRow.gb-rowActive{border-radius:6px;background:var(--dsw-alias-bg-layer-2,transparent)}",
1630
+ ".gb-toolState.gb-st-running{border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-brand-primary);display:inline-flex;align-items:center;gap:5px}",
1631
+ ".gb-st-ok{border-color:var(--dsw-alias-state-success-primary,#3fb950);color:var(--dsw-alias-state-success-primary,#3fb950)}",
1632
+ ".gb-st-fail{border-color:var(--dsw-alias-status-danger,#e5484d);color:var(--dsw-alias-status-danger,#e5484d)}",
1633
+ ".gb-spin{flex:none;display:inline-block;width:10px;height:10px;border:2px solid var(--dsw-alias-border-l2);border-top-color:var(--dsw-alias-brand-primary);border-radius:50%;animation:gb-spin .8s linear infinite}",
1634
+ ".gb-toolBar{flex:1 1 auto;min-width:32px;height:4px;border-radius:2px;background:var(--dsw-alias-border-l2);overflow:hidden}",
1635
+ ".gb-toolBarFill{display:block;height:100%;width:38%;border-radius:2px;background:var(--dsw-alias-brand-primary);animation:gb-slide 1.15s ease-in-out infinite}",
1636
+ ".gb-jobBar{height:6px;border-radius:3px;background:var(--dsw-alias-border-l2);overflow:hidden}",
1637
+ ".gb-jobBarFill{display:block;height:100%;border-radius:3px;background:var(--dsw-alias-brand-primary);transition:width .3s ease}",
1638
+ ".gb-toolFail{margin:0 0 4px 26px;font-size:11.5px;line-height:1.5;color:var(--dsw-alias-status-danger,#e5484d);overflow-wrap:anywhere}",
1639
+ "@keyframes gb-spin{to{transform:rotate(360deg)}}",
1640
+ "@keyframes gb-slide{0%{margin-left:-38%}100%{margin-left:100%}}",
1584
1641
  ].join("\n");
1585
1642
 
1586
1643
  function ensureStyles() {
@@ -2027,8 +2084,23 @@ window.__ModuleLoader__.load({
2027
2084
  even be ticked, so an upgrade can never touch it. */
2028
2085
  var toolRows = toolsData !== null && Array.isArray(toolsData.tools) ? toolsData.tools : [];
2029
2086
  var wingetOk = !!(toolsData !== null && toolsData.winget && toolsData.winget.ok);
2087
+ /* The job lives on the HOST and keeps running long after the POST has
2088
+ returned, so "busy" must be derived from the JOB, not from the request.
2089
+ Keying the buttons off the POST alone re-enabled them the instant the
2090
+ host answered 202 — which is precisely how a second click could land on
2091
+ an already-running batch. */
2092
+ var jobRunning = toolJob !== null && toolJob.finished !== true;
2093
+ var busyNow = toolBusy || jobRunning;
2094
+ var jobById = {};
2095
+ if (toolJob !== null && Array.isArray(toolJob.results)) {
2096
+ toolJob.results.forEach(function (entry) {
2097
+ if (entry !== null && typeof entry === "object" && typeof entry.id === "string") jobById[entry.id] = entry;
2098
+ });
2099
+ }
2100
+ var activeCmd = jobRunning && typeof toolJob.current === "string" ? toolJob.current : "";
2101
+ var jobPct = jobRunning && toolJob.total > 0 ? Math.round(toolJob.done / toolJob.total * 100) : 0;
2030
2102
  function toolSelectable(row) {
2031
- return wingetOk && !toolBusy && (row.state === "missing" || row.state === "managed");
2103
+ return wingetOk && !busyNow && (row.state === "missing" || row.state === "managed");
2032
2104
  }
2033
2105
  var toolSelCount = toolRows.filter(function (row) { return toolSel[row.id] === true; }).length;
2034
2106
 
@@ -2126,8 +2198,31 @@ window.__ModuleLoader__.load({
2126
2198
  toolErr !== "" ? E("p", { className: "gb-error" }, toolErr) : null,
2127
2199
  toolsData !== null && !wingetOk ? E("p", { className: "gb-error" }, t("tools.nowinget")) : null,
2128
2200
  toolRows.length > 0 ? E("div", { className: "gb-tools" },
2129
- toolRows.map(function (row) {
2130
- return E("label", { key: row.id, className: "gb-toolRow", title: row.publisher + " · " + row.source },
2201
+ /* ONE row per tool, and the row itself carries the progress: the
2202
+ tool being worked on shows a spinner, whether it is being
2203
+ installed or upgraded, and a sweeping bar; a finished tool keeps
2204
+ its verdict; a failure prints its reason directly underneath, so
2205
+ nothing has to be hunted for in a separate log area. */
2206
+ toolRows.reduce(function (acc, row) {
2207
+ var entry = jobById[row.id];
2208
+ var active = activeCmd !== "" && activeCmd === row.cmd;
2209
+ var isUpgrade = row.state === "managed";
2210
+ var badge;
2211
+ if (active) {
2212
+ badge = E("span", { className: "gb-toolState gb-st-running" },
2213
+ E("span", { className: "gb-spin" }),
2214
+ t(isUpgrade ? "tools.updating" : "tools.installing"));
2215
+ } else if (entry !== undefined) {
2216
+ badge = E("span", { className: "gb-toolState " + (entry.ok ? "gb-st-ok" : "gb-st-fail") },
2217
+ (entry.ok ? "✓ " : "✗ ") + t(entry.ok ? "tools.resultOk" : "tools.resultFail"));
2218
+ } else {
2219
+ badge = E("span", { className: "gb-toolState gb-st-" + row.state }, t("tools.state." + row.state));
2220
+ }
2221
+ acc.push(E("label", {
2222
+ key: row.id,
2223
+ className: "gb-toolRow" + (active ? " gb-rowActive" : ""),
2224
+ title: row.publisher + " · " + row.source,
2225
+ },
2131
2226
  E("input", {
2132
2227
  type: "checkbox",
2133
2228
  checked: toolSel[row.id] === true,
@@ -2135,27 +2230,39 @@ window.__ModuleLoader__.load({
2135
2230
  onChange: function () { toggleTool(row.id); },
2136
2231
  }),
2137
2232
  E("span", { className: "gb-toolCmd" }, row.cmd),
2138
- E("span", { className: "gb-toolState gb-st-" + row.state }, t("tools.state." + row.state)),
2233
+ badge,
2234
+ active ? E("span", { className: "gb-toolBar" }, E("span", { className: "gb-toolBarFill" })) : null,
2139
2235
  E("span", { className: "gb-toolSrc" }, (row.version !== "" ? row.version + " · " : "") + (row.provider !== "" ? row.provider : row.source) + (row.admin ? " · " + t("tools.adminHint") : "")),
2140
- );
2141
- }),
2236
+ ));
2237
+ if (entry !== undefined && entry.ok !== true) {
2238
+ acc.push(E("p", { key: row.id + ":fail", className: "gb-toolFail" },
2239
+ t(entry.action === "upgrade" ? "tools.updating" : "tools.installing") + " — " + t("tools.resultFail") + ": " + (entry.detail || "")));
2240
+ }
2241
+ return acc;
2242
+ }, []),
2142
2243
  ) : null,
2143
2244
  toolsData !== null && wingetOk ? E("div", { className: "gb-seg" },
2144
- E("button", { type: "button", className: "gb-segBtn", disabled: toolBusy, onClick: function () { loadTools(); } }, t("tools.refresh")),
2145
- E("button", { type: "button", className: "gb-segBtn", disabled: toolBusy, onClick: selectInstallable }, t("tools.selectMissing")),
2146
- E("button", { type: "button", className: "gb-segBtn" + (toolSelCount > 0 ? " gb-segActive" : ""), disabled: toolBusy || toolSelCount === 0, onClick: runTools }, t("tools.run")),
2245
+ E("button", { type: "button", className: "gb-segBtn", disabled: busyNow, onClick: function () { loadTools(); } }, t("tools.refresh")),
2246
+ E("button", { type: "button", className: "gb-segBtn", disabled: busyNow, onClick: selectInstallable }, t("tools.selectMissing")),
2247
+ /* A running job DISABLES this button and says so — the host keeps
2248
+ working after the POST returns, so re-enabling on the response
2249
+ would invite a second click onto a batch already in flight. */
2250
+ E("button", {
2251
+ type: "button",
2252
+ className: "gb-segBtn" + (toolSelCount > 0 && !busyNow ? " gb-segActive" : ""),
2253
+ disabled: busyNow || toolSelCount === 0,
2254
+ onClick: runTools,
2255
+ }, busyNow ? t("tools.working") : t("tools.run")),
2147
2256
  ) : null,
2148
- toolJob !== null ? E("div", { className: "gb-stack" },
2257
+ jobRunning ? E("div", { className: "gb-stack" },
2149
2258
  E("div", { className: "gb-row" },
2150
- E("span", { className: "gb-rowLabel" }, t("tools.working") + ":"),
2151
- E("span", { className: "gb-rowValue" }, (toolJob.current ? toolJob.current + " " : "") + toolJob.done + " / " + toolJob.total),
2259
+ E("span", { className: "gb-spin" }),
2260
+ E("span", { className: "gb-rowLabel" }, (activeCmd !== "" ? activeCmd + " · " : "") + t("tools.working") + ":"),
2261
+ E("span", { className: "gb-rowValue" }, toolJob.done + " / " + toolJob.total),
2262
+ ),
2263
+ E("div", { className: "gb-jobBar" },
2264
+ E("div", { className: "gb-jobBarFill", style: { width: jobPct + "%" } }),
2152
2265
  ),
2153
- (toolJob.results || []).map(function (item) {
2154
- return E("div", { key: item.id, className: "gb-toolResult" },
2155
- E("span", { className: item.ok ? "gb-ok" : "gb-bad" }, item.cmd + " " + t(item.ok ? "tools.resultOk" : "tools.resultFail")),
2156
- E("span", null, item.detail || ""),
2157
- );
2158
- }),
2159
2266
  ) : null,
2160
2267
  E("p", { className: "gb-hint" }, t("tools.hint")),
2161
2268
  ),
@@ -2345,6 +2452,185 @@ window.__ModuleLoader__.load({
2345
2452
  );
2346
2453
  }
2347
2454
 
2455
+ // ── native right-sidebar path rescue (v0.31.0) ───────────────────────
2456
+ //
2457
+ // WHY: while the POSIX dialect is on, the conversation shows MSYS drive
2458
+ // roots (/c/Users/...), and dsh's OWN file links carry that spelling into
2459
+ // the right Sidebar. The Host resolves the path with node:path against
2460
+ // the session cwd, so '/c/Users/x' becomes '<current drive>:\c\Users\x'
2461
+ // and the preview answers "file not found". The plugin introduced that
2462
+ // spelling, so the plugin restores its meaning — at the ONE point every
2463
+ // way into the column passes through: `ctx.sidebarRight.openResource`
2464
+ // (conversation file links, tool-row line references, the file tree).
2465
+ //
2466
+ // SAFETY: the rewrite is driven by the HOST's own platform fact, never by
2467
+ // a guess: nothing is rewritten until `/api/pathmap` reports win32, so a
2468
+ // macOS/Linux Host can never see its paths translated. A path already in
2469
+ // the Host's spelling is returned unchanged, so with the dialect off the
2470
+ // wrapper is a pure pass-through. The wrapper lives on the service
2471
+ // instance and is removed with this plugin's own fiber.
2472
+ var FILE_ADDRESS_PREFIX = "dsh-resource://file/";
2473
+ var PATHMAP_URL = "dsh-gitbash-shell/api/pathmap";
2474
+ /** The Host's MSYS facts, or null until /api/pathmap has answered. */
2475
+ var pathMap = null;
2476
+ var pathMapRequested = false;
2477
+
2478
+ /** Read the Host's platform + mount facts once; never blocks a click. */
2479
+ function loadPathMap() {
2480
+ if (pathMapRequested) return;
2481
+ pathMapRequested = true;
2482
+ try {
2483
+ Promise.resolve(fetch(PATHMAP_URL, { headers: { accept: "application/json" } }))
2484
+ .then(function (response) { return response && response.ok ? response.json() : null; })
2485
+ .then(function (body) {
2486
+ if (body && typeof body === "object" && typeof body.platform === "string") pathMap = body;
2487
+ else console.warn(TAG + " path map unavailable: no rewrite until the host answers");
2488
+ })
2489
+ .catch(function (error) {
2490
+ console.warn(TAG + " path map fetch failed:", error && error.message ? error.message : error);
2491
+ });
2492
+ } catch (error) {
2493
+ console.warn(TAG + " path map fetch threw:", error && error.message ? error.message : error);
2494
+ }
2495
+ }
2496
+
2497
+ /** Whether the Host is a Windows one whose filesystem needs the rewrite. */
2498
+ function pathRescueActive() {
2499
+ return pathMap !== null && pathMap.platform === "win32";
2500
+ }
2501
+
2502
+ /**
2503
+ * '/c/Users/x' -> 'C:/Users/x', plus the Git Bash mounts the Host's own
2504
+ * translation layer resolves (~, /tmp, /dev/null, /usr & friends). Mirrors
2505
+ * src/index.js translateMsysPath WITHOUT its `$VAR` expansion: a path
2506
+ * rendered in the transcript is text, not a shell word.
2507
+ */
2508
+ function msysToHostPath(value, env) {
2509
+ if (typeof value !== "string" || value === "") return value;
2510
+ var facts = env || {};
2511
+ if (facts.home && (value === "~" || value.slice(0, 2) === "~/")) {
2512
+ return value === "~" ? facts.home : facts.home + "/" + value.slice(2);
2513
+ }
2514
+ var drive = /^\/([a-z])\/(.*)$/i.exec(value);
2515
+ if (drive) return drive[1].toUpperCase() + ":/" + drive[2];
2516
+ var bare = /^\/([a-z])$/i.exec(value);
2517
+ if (bare) return bare[1].toUpperCase() + ":/";
2518
+ if (value === "/dev/null") return "\\\\.\\NUL";
2519
+ if (facts.tmpDir && (value === "/tmp" || value.slice(0, 5) === "/tmp/")) {
2520
+ return value === "/tmp" ? facts.tmpDir : facts.tmpDir + value.slice(4);
2521
+ }
2522
+ if (facts.gitRoot && facts.mounts) {
2523
+ var mounts = facts.mounts;
2524
+ for (var mount in mounts) {
2525
+ if (!Object.prototype.hasOwnProperty.call(mounts, mount)) continue;
2526
+ if (value === mount || value.slice(0, mount.length + 1) === mount + "/") {
2527
+ return facts.gitRoot + "/" + mounts[mount] + (value === mount ? "" : value.slice(mount.length));
2528
+ }
2529
+ }
2530
+ }
2531
+ return value;
2532
+ }
2533
+
2534
+ /** Component-encode one path segment, keeping ':' literal for drive letters. */
2535
+ function encodeAddressSegment(segment) {
2536
+ return encodeURIComponent(segment).replace(/%3A/gi, ":");
2537
+ }
2538
+
2539
+ /** Encode a '/'-separated path the way dsh's own address builder does. */
2540
+ function encodeAddressPath(path) {
2541
+ return path.split("/").map(encodeAddressSegment).join("/");
2542
+ }
2543
+
2544
+ /**
2545
+ * Rewrite one `dsh-resource://file/...` address so its path uses the
2546
+ * Host's spelling. Anything that is not a file address, does not decode,
2547
+ * or does not change comes back untouched — the caller then forwards the
2548
+ * ORIGINAL string, so no URL round trip can alter a path we never
2549
+ * translated.
2550
+ * @param address - the resource address the Sidebar is about to open.
2551
+ * @param env - the Host's mount facts, or null.
2552
+ * @returns the address to open.
2553
+ */
2554
+ function rescueFileAddress(address, env) {
2555
+ if (typeof address !== "string" || address.slice(0, FILE_ADDRESS_PREFIX.length) !== FILE_ADDRESS_PREFIX) return address;
2556
+ var tail = address.slice(FILE_ADDRESS_PREFIX.length);
2557
+ var cut = tail.search(/[?#]/);
2558
+ var suffix = cut === -1 ? "" : tail.slice(cut);
2559
+ var parts = (cut === -1 ? tail : tail.slice(0, cut)).split("/");
2560
+ var scope = parts.shift();
2561
+ if (scope !== "session" && scope !== "absolute") return address;
2562
+ var sessionId = null;
2563
+ if (scope === "session") {
2564
+ sessionId = parts.shift();
2565
+ if (sessionId === undefined || sessionId === "" || parts.length === 0) return address;
2566
+ }
2567
+ var segments;
2568
+ try {
2569
+ segments = parts.map(decodeURIComponent);
2570
+ } catch (error) {
2571
+ return address; // a malformed escape is not ours to repair
2572
+ }
2573
+ // '//server/share' keeps an empty first segment; a UNC path is never an
2574
+ // MSYS drive root, so it is left exactly as the Host spelled it.
2575
+ if (scope === "absolute" && segments[0] === "" && segments.length > 1) return address;
2576
+ var path = scope === "absolute" ? "/" + segments.join("/") : segments.join("/");
2577
+ var translated = msysToHostPath(path, env);
2578
+ if (translated === path) return address;
2579
+ var rebuilt = scope === "session"
2580
+ ? FILE_ADDRESS_PREFIX + "session/" + encodeAddressSegment(sessionId) + "/" + encodeAddressPath(translated)
2581
+ : FILE_ADDRESS_PREFIX + "absolute/" + encodeAddressPath(translated.replace(/^\/+/, ""));
2582
+ return rebuilt + suffix;
2583
+ }
2584
+
2585
+ /**
2586
+ * Wrap the navigation controller's ONE address-taking entry point. Every
2587
+ * way into the right column calls `openResource` (or its per-session twin),
2588
+ * so one wrapper covers the conversation links, the tool rows and the file
2589
+ * tree at once — no dsh source is touched, and the wrapper is removed when
2590
+ * this plugin's fiber is.
2591
+ */
2592
+ function installPathRescue(ctx) {
2593
+ loadPathMap();
2594
+ try {
2595
+ ctx.inject(["sidebarRight"], function (sctx) {
2596
+ try {
2597
+ var controller = sctx && sctx.sidebarRight;
2598
+ if (!controller || controller.gbPathRescue === true) return;
2599
+ var openResource = controller.openResource;
2600
+ if (typeof openResource !== "function") return;
2601
+ var openResourceIn = controller.openResourceIn;
2602
+ var rescue = function (address) {
2603
+ return pathRescueActive() ? rescueFileAddress(address, pathMap) : address;
2604
+ };
2605
+ controller.openResource = function (address, options) {
2606
+ return openResource.call(this, rescue(address), options);
2607
+ };
2608
+ if (typeof openResourceIn === "function") {
2609
+ controller.openResourceIn = function (sessionId, address, options) {
2610
+ return openResourceIn.call(this, sessionId, rescue(address), options);
2611
+ };
2612
+ }
2613
+ controller.gbPathRescue = true;
2614
+ var restore = function () {
2615
+ try {
2616
+ controller.openResource = openResource;
2617
+ if (typeof openResourceIn === "function") controller.openResourceIn = openResourceIn;
2618
+ delete controller.gbPathRescue;
2619
+ } catch (error) { /* a frozen instance keeps the wrapper; harmless */ }
2620
+ };
2621
+ if (typeof sctx.effect === "function") {
2622
+ sctx.effect(function () { return restore; }, "dsh-gitbash-shell: sidebar path rescue");
2623
+ }
2624
+ console.log(TAG + " right-sidebar path rescue armed (host spelling restored on open)");
2625
+ } catch (error) {
2626
+ console.warn(TAG + " right-sidebar path rescue failed:", error && error.message ? error.message : error);
2627
+ }
2628
+ });
2629
+ } catch (error) {
2630
+ console.warn(TAG + " right-sidebar path rescue wiring failed:", error && error.message ? error.message : error);
2631
+ }
2632
+ }
2633
+
2348
2634
  // ── plugin ────────────────────────────────────────────────────────────
2349
2635
 
2350
2636
  exports.name = "dsh-gitbash-shell/client";
@@ -2472,6 +2758,11 @@ window.__ModuleLoader__.load({
2472
2758
  } catch (error) {
2473
2759
  console.warn(TAG + " settings card registration failed:", error && error.message ? error.message : error);
2474
2760
  }
2761
+
2762
+ // The native right Sidebar consumes the very dialect this card gates,
2763
+ // so the rescue is armed on the same mount — and independently of the
2764
+ // card, which must never be taken down by it.
2765
+ installPathRescue(ctx);
2475
2766
  };
2476
2767
 
2477
2768
  return module.exports;
package/src/index.js CHANGED
@@ -2776,6 +2776,62 @@ export async function apply(ctx, config = {}) {
2776
2776
  }
2777
2777
  }
2778
2778
 
2779
+ // ── MSYS path facts for the browser half (v0.31.0) ───────────────────────
2780
+ // The native right Sidebar receives the path spelling the CONVERSATION
2781
+ // shows. While this plugin's POSIX dialect is on, that spelling is an MSYS
2782
+ // drive root (/c/Users/...), and the Host filesystem resolves it against the
2783
+ // current drive instead — '<drive>:\c\Users\...' — so every file link in the
2784
+ // transcript opens onto "file not found". The browser half rewrites the
2785
+ // resource address before opening it; this route is the fact bag that
2786
+ // rewrite needs. It reports the SAME env the tool-argument translation
2787
+ // already resolves against, plus the platform, so a non-Windows client never
2788
+ // rewrites anything. Read-only and loopback-fenced (a GET needs no Origin),
2789
+ // because the web server may be bound to 0.0.0.0.
2790
+ try {
2791
+ ctx.inject(['webServer'], (wctx) => {
2792
+ try {
2793
+ const route = '/dsh-gitbash-shell/api/pathmap'
2794
+ const mounts = {}
2795
+ for (const [mount, target] of GIT_MOUNTS) mounts[mount] = target
2796
+ const send = (res, code, payload) => {
2797
+ const text = JSON.stringify(payload)
2798
+ res.writeHead(code, {
2799
+ 'content-type': 'application/json; charset=utf-8',
2800
+ 'cache-control': 'no-store',
2801
+ 'content-length': Buffer.byteLength(text),
2802
+ })
2803
+ res.end(text)
2804
+ }
2805
+ const handler = (req, res) => {
2806
+ const fence = fenceToolRequest(req)
2807
+ if (!fence.ok) {
2808
+ console.log(TAG + ' path map request refused (' + fence.reason + ')')
2809
+ send(res, 403, { error: 'forbidden', reason: fence.reason })
2810
+ return
2811
+ }
2812
+ const env = buildTranslateEnv(configuredBashPath)
2813
+ // One line per read: the browser half fetches this once per page load,
2814
+ // so its presence in the log is how a user can tell the rescue is live.
2815
+ console.log(TAG + ' path map read by ' + (req.socket && req.socket.remoteAddress ? req.socket.remoteAddress : '?'))
2816
+ send(res, 200, {
2817
+ platform: process.platform,
2818
+ home: env.home ?? null,
2819
+ tmpDir: env.tmpDir ?? null,
2820
+ gitRoot: env.gitRoot ?? null,
2821
+ mounts,
2822
+ })
2823
+ }
2824
+ const dispose = wctx.webServer.register({ kind: 'prefix', path: route, handler })
2825
+ wctx.effect(() => dispose, 'dsh-gitbash-shell: path map route')
2826
+ console.log(TAG + ' MSYS path map route active at ' + route + ' (read by the browser half)')
2827
+ } catch (error) {
2828
+ console.log(TAG + ' path map route failed: ' + (error?.message ?? error))
2829
+ }
2830
+ })
2831
+ } catch (error) {
2832
+ console.log(TAG + ' path map route wiring failed: ' + (error?.message ?? error))
2833
+ }
2834
+
2779
2835
  // ── winget toolchain route (v0.30.0) ─────────────────────────────────────
2780
2836
  // The settings card's "install / upgrade" buttons. Installing software is a
2781
2837
  // real side effect, so src/tool-runner.js fences the route: only a loopback