dsh-gitbash-shell 0.30.1 → 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,101 @@
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
+
6
101
  ## v0.30.1 — 2026-09-26
7
102
 
8
103
  **类型**:fix(用户实测反馈 v0.30.0 的工具列表:点了按钮看不到进度、看不到在装哪一个、看不到报错、还能连点)
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.1",
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.1",
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
@@ -2452,6 +2452,185 @@ window.__ModuleLoader__.load({
2452
2452
  );
2453
2453
  }
2454
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
+
2455
2634
  // ── plugin ────────────────────────────────────────────────────────────
2456
2635
 
2457
2636
  exports.name = "dsh-gitbash-shell/client";
@@ -2579,6 +2758,11 @@ window.__ModuleLoader__.load({
2579
2758
  } catch (error) {
2580
2759
  console.warn(TAG + " settings card registration failed:", error && error.message ? error.message : error);
2581
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);
2582
2766
  };
2583
2767
 
2584
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