dsh-gitbash-shell 0.13.2 → 0.15.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/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  [![Awesome DSH Plugin](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com)
4
4
 
5
+ 简体中文 | [English](README_EN.md)
6
+
5
7
  > 让 DeepSeek Harness (dsh) 在 Windows 上**全部使用 Git Bash** 的官方风格插件
6
8
  > —— 以 Git for Windows 的 `bash.exe` 替换 PowerShell 执行器,并为所有模式
7
9
  > 物化对应的 Git Bash 版 agent preset。
@@ -37,6 +39,19 @@ era。**preset id 保持 `code-gitbash` 不变**(会话钉在 id 上,改名会
37
39
  会话报 preset not found)。无论先升级插件还是先升级 dsh,都会自动收敛;用户改
38
40
  过的目录照旧不碰。
39
41
 
42
+ ### dsh 0.1.6 适配(工作流引擎行改名)
43
+
44
+ dsh 0.1.6-alpha.1 把内置预设的工作流引擎行 `workflow-worker-thread` 改名为
45
+ `workflow-ptc`,并**删除**了旧包。组合里一行 import 失败会拒绝**整棵 preset 挂载**,
46
+ 所以把旧名钉死在资产里的 preset 在新版上会直接不可用。本插件两个拼法都不钉:物化时
47
+ **从宿主自己的内置 preset 现场抄**那一行的 id、包名与 `disabled` 状态
48
+ (`rowFormsOf` / `alignEngineRow`),并让 `tool-ralph` 跟随新版默认的 `disabled: true`。
49
+ 改写是纯字符串手术(不解析 YAML,`!!js` 安全)且幂等;探测失败(旧宿主、无 roster)时
50
+ 资产保持逐字节原样——**一份资产通吃两个 era,升级顺序无关**。
51
+
52
+ 同一版还把 `LocalBashExecutor` 的受保护钩子改成了异步:插件的 `runArgv` 结果解包、
53
+ `start` 的返回形态与 `confine` 的取消信号都按**加载期探测**自适应,新旧宿主行为一致。
54
+
40
55
  ## 安装(公开 npm 插件,推荐)
41
56
 
42
57
  npm: [dsh-gitbash-shell](https://www.npmjs.com/package/dsh-gitbash-shell),
@@ -129,9 +144,30 @@ preset 会被插件自动清理;宿主 shell 回退为 PowerShell。
129
144
  > 切换生效后若 `ptc-cordis` 目录已存在且被旧版本物化,删除
130
145
  > `~/.dsh/.agent-presets/ptc-cordis` 并重启,即由新逻辑重新物化。
131
146
 
132
- ## POSIX 路径指示(v0.7.0)
147
+ ## POSIX 路径方言(v0.7.0 引入,受 `posixPaths` 开关门控)
148
+
149
+ Windows 上本插件把宿主 shell 换成 Git Bash 的同时,让**模型看到的路径**统一成 MSYS 盘根
150
+ POSIX 形式(`/c/Users/...`、`/c/Program Files/...`)。开关是 settings 命名空间
151
+ `gitbash-shell` 的布尔字段 `posixPaths`,**默认开启**(v0.10.0 起),可在 **设置 → 插件**
152
+ 里本插件的「Git Bash 路径方言」卡片上随时切换;不修改任何 preset / 组合文件,标准/极简/
153
+ PTC/创造及用户自建模式一律覆盖。
154
+
155
+ 开启时(仅 Win32),`posixPaths` 门控以下全部行为:
156
+
157
+ - **提示词源头替换**:组装期(`system-prompt/assemble`)把官方提示词 sections / contexts /
158
+ variables 里的 Windows 绝对路径**原位**改写成 `/c/...`——不增删任何内容、不动工具 schema;
159
+ - **一句话指示**:经 `systemPrompt.context`(order 126)注入全局指示——shell 是 Git for
160
+ Windows bash,路径用 MSYS 盘根,所有工具都直接接受这种写法;
161
+ - **参数翻译**:`tools/execute` 上把文件工具的路径参数(`file_path` / `path` / `workdir`)由
162
+ `/c/...` 翻回 `C:/...` 交给 Node 文件工具;bash 的 `command` 字段不动——那是 Git Bash 母语;
163
+ - **结果回流**:成功结果里的路径元数据(`read`/`write`/`edit` 的 `path`、`glob` 的 `paths[]`、
164
+ `grep` 的 `matches[].path`)改写回 MSYS 形式;文件内容与错误结果不动;
165
+ - **运行时事实**:向官方 `dsh-shell-env` 注册表贡献 `DSH_PATH_DIALECT=msys`,模型可在执行时
166
+ 核验(随开关实时生效)。
133
167
 
134
- Windows 上本插件把宿主 shell 换成 Git Bash 的同时,会向**每个会话的系统提示**注入一条全局指示(仅 Win32,走 `systemPrompt.context`,不影响任何 preset/组合文本):**bash 命令里一律用 POSIX 风格路径**(`/c/Users/...`、`/c/Program Files/...`),不要 `C:/...` 或 `C:\...`。这样模型在工具调用里不会再拿 Windows 盘符路径喂给 Git Bash——标准/极简/PTC/创造及用户自建模式全部覆盖,无需任何模式单独改。
168
+ 关闭后回到 dsh 原生行为:指示文本为空(组装期直接丢弃,零提示噪声),路径参数与结果元数据
169
+ 都不再改写,文件工具收 Windows 路径。**bash 始终是 Git Bash,不受此开关影响**——它只决定
170
+ 跨工具的路径方言。
135
171
 
136
172
  ## 与 dsh-better-sidebar 联动
137
173
 
@@ -143,4 +179,4 @@ Windows 上本插件把宿主 shell 换成 Git Bash 的同时,会向**每个会
143
179
 
144
180
  ## 许可
145
181
 
146
- MIT © KannaKuron。参考与致敬:[dsh-ptc-cordis-preset](https://github.com/KannaKuron/dsh-ptc-cordis-preset)。
182
+ MIT © KannaKuron。参考与致敬:[dsh-ptc-cordis-preset](https://github.com/KannaKuron/dsh-ptc-cordis-preset)。
package/README_EN.md CHANGED
@@ -2,10 +2,29 @@
2
2
 
3
3
  [![Awesome DSH Plugin](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com)
4
4
 
5
+ [简体中文](README.md) | English
6
+
5
7
  > Run **every agent shell command through Git Bash** on Windows with DeepSeek
6
8
  > Harness (dsh) — replaces the PowerShell executor and materializes Git Bash
7
9
  > variants of all four agent presets.
8
10
 
11
+ ### dsh 0.1.6: the workflow-engine row was renamed
12
+
13
+ dsh 0.1.6-alpha.1 renamed the built-in presets' workflow-engine row from
14
+ `workflow-worker-thread` to `workflow-ptc` and **deleted** the old package. One row
15
+ that fails to import rejects the **whole preset mount**, so a composition pinning the
16
+ old name simply stops working on the new host. This plugin pins neither spelling: at
17
+ materialization it copies that row — id, package and `disabled` state — straight out
18
+ of the host's own built-in preset (`rowFormsOf` / `alignEngineRow`), and aligns
19
+ `tool-ralph` with the new `disabled: true` default. The rewrite is plain string
20
+ surgery (no YAML round-trip, so `!!js` stays safe) and idempotent; a failed probe (old
21
+ host, no roster) leaves the assets byte-for-byte untouched — **one set of assets serves
22
+ both eras, in either upgrade order**.
23
+
24
+ The same release made `LocalBashExecutor`'s protected hooks asynchronous: the
25
+ `runArgv` result unwrapping, the `start` return shape and the `confine` cancellation
26
+ signal are all probed at load time, so both host generations behave identically.
27
+
9
28
  ## Install (public npm package)
10
29
 
11
30
  ```sh
@@ -64,9 +83,36 @@ the PTC preset is materialized as Git Bash automatically (tool-bash on,
64
83
  tool-pwsh off) — no extra mode, no manual edits. Without this plugin the PTC
65
84
  preset stays as its own plugin manages it.
66
85
 
67
- ## POSIX path directive (v0.7.0)
68
-
69
- While replacing the host shell with Git Bash, this plugin also injects one global per-session system-prompt directive (Win32 only, via systemPrompt.context; no preset or composition text is touched): **always use POSIX-style paths in bash commands** (/c/Users/..., /c/Program Files/...) — never C:/... or C:\... . Every mode — standard/minimal/PTC/creation and user-authored presets — is covered; nothing needs a per-mode change.
86
+ ## POSIX path dialect (introduced in v0.7.0, gated by the `posixPaths` switch)
87
+
88
+ While replacing the host shell with Git Bash on Windows, this plugin makes every path the
89
+ model sees use the MSYS drive-root POSIX form (/c/Users/..., /c/Program Files/...). The switch
90
+ is the `posixPaths` boolean in the `gitbash-shell` settings namespace; it is **on by default**
91
+ (since v0.10.0) and can be toggled any time on this plugin's "Git Bash path dialect" card in
92
+ **Settings → Plugins**. No preset or composition file is touched, and every mode —
93
+ standard/minimal/PTC/creation and user-authored presets — is covered.
94
+
95
+ While it is on (Windows only), `posixPaths` gates all of the following:
96
+
97
+ - **Source-level rewrite**: during assembly (`system-prompt/assemble`) every Windows absolute
98
+ path in the official prompt's sections, contexts, and variables is rewritten in place to the
99
+ /c/... form — nothing is added or removed, and tool schemas stay untouched;
100
+ - **One-sentence directive**: a global directive via `systemPrompt.context` (order 126) — the
101
+ shell is Git for Windows bash, paths use MSYS drive roots, and every tool accepts that form
102
+ directly;
103
+ - **Argument translation**: on `tools/execute` the file tools' path arguments (`file_path` /
104
+ `path` / `workdir`) are translated from /c/... back to C:/... for the Node-backed file tools;
105
+ a bash command's `command` field is left alone — that is Git Bash's native form;
106
+ - **Result round-trip**: path metadata in successful results (`path` of read/write/edit,
107
+ `paths[]` of glob, `matches[].path` of grep) flows back in the MSYS form; file contents and
108
+ error results are untouched;
109
+ - **Runtime fact**: `DSH_PATH_DIALECT=msys` is contributed to the official `dsh-shell-env`
110
+ registry, so the model can verify the dialect at execution time (it follows the live switch).
111
+
112
+ With the switch off, dsh-native behavior returns: the directive text is empty (dropped at
113
+ assembly, so zero prompt noise), path arguments and result metadata are no longer rewritten, and
114
+ the file tools receive Windows paths. **Bash stays Git Bash either way** — the switch only
115
+ governs the cross-tool path dialect.
70
116
 
71
117
  ## Cooperation with dsh-better-sidebar
72
118
 
@@ -79,4 +125,4 @@ When [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar) (v0.1
79
125
  ## License
80
126
 
81
127
  MIT © KannaKuron. Inspired by
82
- [dsh-ptc-cordis-preset](https://github.com/KannaKuron/dsh-ptc-cordis-preset).
128
+ [dsh-ptc-cordis-preset](https://github.com/KannaKuron/dsh-ptc-cordis-preset).
package/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-external/dsh-gitbash-shell",
3
- "version": "0.13.2",
3
+ "version": "0.15.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.13.2",
3
+ "version": "0.15.0",
4
4
  "description": "DSH plugin: run every agent shell command through Git for Windows bash on Windows instead of PowerShell. Replaces the pwsh executor with a Git Bash ctx.shell provider and materializes Git Bash variants of the standard/minimal/code/cordis agent presets into your user preset root at startup. On Windows it also adopts dsh-better-sidebar's terminal shell (UI tabs and the model-facing terminal_* tools) through the sidebar's official settings seam, and pairs with dsh-ptc-cordis-preset so the PTC Creation-mode preset materializes as its Git Bash variant. Ships both composition eras for dsh <= 0.1.1 (built-in preset id 'code') and dsh >= 0.1.2 (renamed id 'ptc') — the era is probed per boot and re-materialized automatically after a dsh upgrade, in either upgrade order; preset ids never change so pinned sessions stay valid.",
5
5
  "type": "module",
6
6
  "repository": {
package/src/client.js CHANGED
@@ -21,7 +21,11 @@
21
21
  * (react and @deepseek-ai/dsh-client-ui-primitives only, smoke-enforced);
22
22
  * - plain React.createElement, no JSX/TS; components at module level;
23
23
  * - dsh.client.inject in package.json lists the packages that must load
24
- * first so locale / settingsScope / slots exist when this applies.
24
+ * first so locale / settingsScope / slots exist when this applies;
25
+ * - copy ships in three layers: zh/en inline, one LOCALES entry per third
26
+ * language (each preceded by a locale marker line), every dictionary
27
+ * key-aligned with zh (smoke-enforced) and read through a live lookup —
28
+ * DSH's language preference switches without a page reload.
25
29
  */
26
30
  window.__ModuleLoader__.load({
27
31
  id: "dsh-gitbash-shell",
@@ -35,6 +39,7 @@ window.__ModuleLoader__.load({
35
39
 
36
40
  var E = React.createElement;
37
41
  var useState = React.useState;
42
+ var useEffect = React.useEffect;
38
43
 
39
44
  var TAG = "[gitbash-shell/client]";
40
45
  /** Dictionary namespace (the plugin's own t seat). */
@@ -42,11 +47,15 @@ window.__ModuleLoader__.load({
42
47
  /** The settings namespace the HOST half serves. */
43
48
  var SETTINGS_NAMESPACE = "gitbash-shell";
44
49
 
45
- // ── dictionaries (zh/en MUST stay key-aligned; smoke-enforced) ─────────
50
+ // ── dictionaries ────────────────────────────────────────────────────────
51
+ // zh/en stay inline; the third languages live in LOCALES below, one entry
52
+ // per language behind a locale marker line. EVERY shipped dictionary MUST
53
+ // carry zh's exact key set: a missing key falls back to English at lookup
54
+ // time, which reads as a half-translated card (smoke-enforced).
46
55
 
47
56
  var zh = {
48
57
  "title": "Git Bash 路径方言",
49
- "cardDesc": "所有工具统一 /c/ POSIX 路径形式(默认关闭)",
58
+ "cardDesc": "所有工具统一 /c/ POSIX 路径形式(默认开启)",
50
59
  "state.label": "当前状态",
51
60
  "state.on": "已启用",
52
61
  "state.off": "已关闭",
@@ -58,7 +67,7 @@ window.__ModuleLoader__.load({
58
67
 
59
68
  var en = {
60
69
  "title": "Git Bash path dialect",
61
- "cardDesc": "One /c/ POSIX path style for every tool (off by default)",
70
+ "cardDesc": "One /c/ POSIX path style for every tool (on by default)",
62
71
  "state.label": "Current state",
63
72
  "state.on": "Enabled",
64
73
  "state.off": "Disabled",
@@ -68,6 +77,302 @@ window.__ModuleLoader__.load({
68
77
  "hint": "Enabled: every tool (bash commands and workdir, read/write/edit/read_image/glob/grep path arguments) uses MSYS drive-root POSIX paths (/c/Users/...); the host translates path fields for the file tools automatically. Disabled restores dsh-native behavior (Windows paths for file tools). Bash is always Git Bash while the plugin is installed; this switch only governs the cross-tool path dialect.",
69
78
  };
70
79
 
80
+ /* Third-language dictionaries: one entry per language, each preceded
81
+ by a locale marker line the smoke test slices on. Adding a language is
82
+ one entry here and nothing else — the dictionaries also ride the
83
+ ctx.locale.register call below, so host-side consumers read the same
84
+ copy the card does. Every key set MUST equal zh's exactly: a missing
85
+ key falls back to English silently and the card looks half-translated.
86
+ zh-hk and zh-mo ship one Cantonese-flavoured copy; zh-tw carries the
87
+ Taiwan wording. */
88
+ var LOCALES = {
89
+ /* locale: ar */
90
+ "ar": {
91
+ "title": "لهجة مسارات Git Bash",
92
+ "cardDesc": "صيغة مسار POSIX موحّدة /c/ لكل الأدوات (مُفعَّلة افتراضيًا)",
93
+ "state.label": "الحالة الحالية",
94
+ "state.on": "مُفعَّلة",
95
+ "state.off": "مُعطَّلة",
96
+ "switch.on": "تفعيل",
97
+ "switch.off": "تعطيل",
98
+ "error": "فشل الكتابة",
99
+ "hint": "عند التفعيل: تستخدم كل الأدوات (أوامر bash و workdir، ومعاملات المسار في read/write/edit/read_image/glob/grep) مسارات POSIX من جذر قرص MSYS (/c/Users/...)؛ ويتولّى المضيف ترجمة حقول المسار لأدوات الملفات تلقائيًا. عند التعطيل يعود السلوك الأصلي لـ dsh (مسارات Windows لأدوات الملفات). يبقى bash دائمًا Git Bash ما دامت الإضافة مثبَّتة؛ هذا المفتاح لا يحكم سوى لهجة المسارات بين الأدوات.",
100
+ },
101
+ /* locale: de */
102
+ "de": {
103
+ "title": "Git-Bash-Pfaddialekt",
104
+ "cardDesc": "Eine einheitliche /c/-POSIX-Pfadform für alle Tools (standardmäßig an)",
105
+ "state.label": "Aktueller Status",
106
+ "state.on": "Aktiviert",
107
+ "state.off": "Deaktiviert",
108
+ "switch.on": "Aktivieren",
109
+ "switch.off": "Deaktivieren",
110
+ "error": "Schreiben fehlgeschlagen",
111
+ "hint": "Aktiviert: Alle Tools (bash-Befehle und workdir sowie Pfadargumente von read/write/edit/read_image/glob/grep) verwenden POSIX-Pfade ab der MSYS-Laufwerkswurzel (/c/Users/...); der Host übersetzt die Pfadfelder der Dateitools automatisch. Deaktiviert stellt das dsh-native Verhalten wieder her (Windows-Pfade für Dateitools). Bash ist bei installiertem Plugin immer Git Bash; dieser Schalter steuert nur den Pfaddialekt zwischen den Tools.",
112
+ },
113
+ /* locale: fr */
114
+ "fr": {
115
+ "title": "Dialecte de chemins Git Bash",
116
+ "cardDesc": "Une forme de chemin POSIX /c/ unique pour tous les outils (activée par défaut)",
117
+ "state.label": "État actuel",
118
+ "state.on": "Activé",
119
+ "state.off": "Désactivé",
120
+ "switch.on": "Activer",
121
+ "switch.off": "Désactiver",
122
+ "error": "échec de l'écriture",
123
+ "hint": "Activé : tous les outils (commandes bash et workdir, arguments de chemin de read/write/edit/read_image/glob/grep) utilisent des chemins POSIX à racine de lecteur MSYS (/c/Users/...) ; l'hôte traduit automatiquement les champs de chemin des outils de fichiers. Désactivé rétablit le comportement natif de dsh (chemins Windows pour les outils de fichiers). Bash reste toujours Git Bash tant que le plugin est installé ; ce commutateur ne régit que le dialecte de chemins entre outils.",
124
+ },
125
+ /* locale: hi */
126
+ "hi": {
127
+ "title": "Git Bash पथ शैली",
128
+ "cardDesc": "सभी टूल के लिए एक ही /c/ POSIX पथ रूप (डिफ़ॉल्ट रूप से सक्षम)",
129
+ "state.label": "वर्तमान स्थिति",
130
+ "state.on": "सक्षम",
131
+ "state.off": "अक्षम",
132
+ "switch.on": "सक्षम करें",
133
+ "switch.off": "अक्षम करें",
134
+ "error": "लिखना विफल रहा",
135
+ "hint": "सक्षम होने पर: सभी टूल (bash कमांड और workdir, तथा read/write/edit/read_image/glob/grep के पथ तर्क) MSYS ड्राइव-रूट POSIX पथ (/c/Users/...) का उपयोग करते हैं; फ़ाइल टूल के पथ फ़ील्ड होस्ट स्वतः अनुवादित करता है। अक्षम करने पर dsh का मूल व्यवहार बहाल होता है (फ़ाइल टूल के लिए Windows पथ)। जब तक प्लगइन इंस्टॉल है bash हमेशा Git Bash रहता है; यह स्विच केवल टूल के बीच पथ शैली नियंत्रित करता है।",
136
+ },
137
+ /* locale: id */
138
+ "id": {
139
+ "title": "Dialek path Git Bash",
140
+ "cardDesc": "Satu bentuk path POSIX /c/ untuk semua alat (aktif secara bawaan)",
141
+ "state.label": "Status saat ini",
142
+ "state.on": "Aktif",
143
+ "state.off": "Nonaktif",
144
+ "switch.on": "Aktifkan",
145
+ "switch.off": "Nonaktifkan",
146
+ "error": "gagal menulis",
147
+ "hint": "Saat aktif: semua alat (perintah bash dan workdir, serta argumen path read/write/edit/read_image/glob/grep) memakai path POSIX berakar drive MSYS (/c/Users/...); host menerjemahkan bidang path untuk alat berkas secara otomatis. Saat nonaktif, perilaku asli dsh dipulihkan (path Windows untuk alat berkas). Bash selalu Git Bash selama plugin terpasang; sakelar ini hanya mengatur dialek path antaralat.",
148
+ },
149
+ /* locale: it */
150
+ "it": {
151
+ "title": "Dialetto dei percorsi Git Bash",
152
+ "cardDesc": "Un'unica forma di percorso POSIX /c/ per tutti gli strumenti (attivata per impostazione predefinita)",
153
+ "state.label": "Stato attuale",
154
+ "state.on": "Attivato",
155
+ "state.off": "Disattivato",
156
+ "switch.on": "Attiva",
157
+ "switch.off": "Disattiva",
158
+ "error": "scrittura non riuscita",
159
+ "hint": "Attivato: tutti gli strumenti (comandi bash e workdir, argomenti di percorso di read/write/edit/read_image/glob/grep) usano percorsi POSIX con radice di unità MSYS (/c/Users/...); l'host traduce automaticamente i campi di percorso degli strumenti per i file. Disattivato ripristina il comportamento nativo di dsh (percorsi Windows per gli strumenti per i file). Bash resta sempre Git Bash finché il plugin è installato; questo interruttore governa solo il dialetto dei percorsi tra gli strumenti.",
160
+ },
161
+ /* locale: ja */
162
+ "ja": {
163
+ "title": "Git Bash パス方言",
164
+ "cardDesc": "すべてのツールで /c/ POSIX パス形式に統一(既定ではオン)",
165
+ "state.label": "現在の状態",
166
+ "state.on": "有効",
167
+ "state.off": "無効",
168
+ "switch.on": "有効にする",
169
+ "switch.off": "無効にする",
170
+ "error": "書き込みに失敗しました",
171
+ "hint": "有効にすると、すべてのツール(bash コマンドと workdir、read/write/edit/read_image/glob/grep のパス引数)が MSYS ドライブルートの POSIX パス(/c/Users/...)を使います。ファイルツールのパスフィールドはホストが自動的に変換します。無効にすると dsh 本来の動作(ファイルツールは Windows パス)に戻ります。bash はプラグインがインストールされている限り常に Git Bash で、このスイッチの影響を受けません。",
172
+ },
173
+ /* locale: ko */
174
+ "ko": {
175
+ "title": "Git Bash 경로 방언",
176
+ "cardDesc": "모든 도구에 하나의 /c/ POSIX 경로 형식 사용(기본값 켜짐)",
177
+ "state.label": "현재 상태",
178
+ "state.on": "사용",
179
+ "state.off": "사용 안 함",
180
+ "switch.on": "사용",
181
+ "switch.off": "사용 안 함",
182
+ "error": "쓰기 실패",
183
+ "hint": "켜면 모든 도구(bash 명령과 workdir, read/write/edit/read_image/glob/grep의 경로 인수)가 MSYS 드라이브 루트 POSIX 경로(/c/Users/...)를 사용합니다. 파일 도구의 경로 필드는 호스트가 자동으로 변환합니다. 끄면 dsh 기본 동작(파일 도구에 Windows 경로 사용)으로 돌아갑니다. 플러그인이 설치되어 있는 동안 bash는 항상 Git Bash이며 이 스위치의 영향을 받지 않습니다.",
184
+ },
185
+ /* locale: nl */
186
+ "nl": {
187
+ "title": "Git Bash-paddialect",
188
+ "cardDesc": "Eén /c/ POSIX-padvorm voor alle tools (standaard aan)",
189
+ "state.label": "Huidige status",
190
+ "state.on": "Ingeschakeld",
191
+ "state.off": "Uitgeschakeld",
192
+ "switch.on": "Inschakelen",
193
+ "switch.off": "Uitschakelen",
194
+ "error": "schrijven mislukt",
195
+ "hint": "Ingeschakeld: alle tools (bash-opdrachten en workdir, padargumenten van read/write/edit/read_image/glob/grep) gebruiken POSIX-paden vanaf de MSYS-schijfwortel (/c/Users/...); de host vertaalt de padvelden voor de bestandstools automatisch. Uitgeschakeld herstelt het oorspronkelijke dsh-gedrag (Windows-paden voor bestandstools). Bash is altijd Git Bash zolang de plugin is geïnstalleerd; deze schakelaar bepaalt alleen het paddialect tussen de tools.",
196
+ },
197
+ /* locale: pl */
198
+ "pl": {
199
+ "title": "Dialekt ścieżek Git Bash",
200
+ "cardDesc": "Jedna postać ścieżki POSIX /c/ dla wszystkich narzędzi (domyślnie włączone)",
201
+ "state.label": "Bieżący stan",
202
+ "state.on": "Włączone",
203
+ "state.off": "Wyłączone",
204
+ "switch.on": "Włącz",
205
+ "switch.off": "Wyłącz",
206
+ "error": "zapis nie powiódł się",
207
+ "hint": "Po włączeniu: wszystkie narzędzia (polecenia bash i workdir oraz argumenty ścieżek w read/write/edit/read_image/glob/grep) używają ścieżek POSIX od korzenia dysku MSYS (/c/Users/...); host automatycznie tłumaczy pola ścieżek narzędzi plikowych. Wyłączenie przywraca natywne zachowanie dsh (ścieżki Windows dla narzędzi plikowych). Bash jest zawsze Git Bash, dopóki wtyczka jest zainstalowana; ten przełącznik reguluje wyłącznie dialekt ścieżek między narzędziami.",
208
+ },
209
+ /* locale: pt */
210
+ "pt": {
211
+ "title": "Dialeto de caminhos do Git Bash",
212
+ "cardDesc": "Uma única forma de caminho POSIX /c/ para todas as ferramentas (ativada por padrão)",
213
+ "state.label": "Estado atual",
214
+ "state.on": "Ativada",
215
+ "state.off": "Desativada",
216
+ "switch.on": "Ativar",
217
+ "switch.off": "Desativar",
218
+ "error": "falha ao gravar",
219
+ "hint": "Ativada: todas as ferramentas (comandos bash e workdir, argumentos de caminho de read/write/edit/read_image/glob/grep) usam caminhos POSIX a partir da raiz da unidade MSYS (/c/Users/...); o host traduz automaticamente os campos de caminho das ferramentas de arquivo. Desativada restaura o comportamento nativo do dsh (caminhos Windows para as ferramentas de arquivo). O bash é sempre Git Bash enquanto o plugin estiver instalado; este interruptor rege apenas o dialeto de caminhos entre ferramentas.",
220
+ },
221
+ /* locale: ru */
222
+ "ru": {
223
+ "title": "Диалект путей Git Bash",
224
+ "cardDesc": "Единый POSIX-формат путей /c/ для всех инструментов (по умолчанию включено)",
225
+ "state.label": "Текущее состояние",
226
+ "state.on": "Включено",
227
+ "state.off": "Выключено",
228
+ "switch.on": "Включить",
229
+ "switch.off": "Выключить",
230
+ "error": "не удалось записать",
231
+ "hint": "Включено: все инструменты (команды bash и workdir, аргументы путей read/write/edit/read_image/glob/grep) используют POSIX-пути от корня диска MSYS (/c/Users/...); хост автоматически преобразует поля путей для файловых инструментов. Выключение возвращает исходное поведение dsh (пути Windows для файловых инструментов). Пока плагин установлен, bash всегда остаётся Git Bash; этот переключатель управляет только диалектом путей между инструментами.",
232
+ },
233
+ /* locale: sv */
234
+ "sv": {
235
+ "title": "Git Bash-sökvägsdialekt",
236
+ "cardDesc": "En /c/ POSIX-sökvägsform för alla verktyg (på som standard)",
237
+ "state.label": "Aktuell status",
238
+ "state.on": "Aktiverad",
239
+ "state.off": "Inaktiverad",
240
+ "switch.on": "Aktivera",
241
+ "switch.off": "Inaktivera",
242
+ "error": "skrivning misslyckades",
243
+ "hint": "Aktiverad: alla verktyg (bash-kommandon och workdir, sökvägsargument för read/write/edit/read_image/glob/grep) använder POSIX-sökvägar från MSYS-enhetsroten (/c/Users/...); värden översätter sökvägsfälten för filverktygen automatiskt. Inaktiverad återställer dsh:s ursprungliga beteende (Windows-sökvägar för filverktyg). Bash är alltid Git Bash så länge insticksprogrammet är installerat; den här växeln styr bara sökvägsdialekten mellan verktygen.",
244
+ },
245
+ /* locale: th */
246
+ "th": {
247
+ "title": "รูปแบบพาธของ Git Bash",
248
+ "cardDesc": "ใช้รูปแบบพาธ POSIX /c/ แบบเดียวกันกับทุกเครื่องมือ (เปิดไว้เป็นค่าเริ่มต้น)",
249
+ "state.label": "สถานะปัจจุบัน",
250
+ "state.on": "เปิดใช้งาน",
251
+ "state.off": "ปิดใช้งาน",
252
+ "switch.on": "เปิดใช้งาน",
253
+ "switch.off": "ปิดใช้งาน",
254
+ "error": "เขียนไม่สำเร็จ",
255
+ "hint": "เมื่อเปิดใช้งาน: เครื่องมือทั้งหมด (คำสั่ง bash และ workdir รวมถึงอาร์กิวเมนต์พาธของ read/write/edit/read_image/glob/grep) ใช้พาธ POSIX จากรากไดรฟ์ของ MSYS (/c/Users/...) โฮสต์จะแปลฟิลด์พาธของเครื่องมือจัดการไฟล์ให้อัตโนมัติ เมื่อปิดใช้งานจะกลับสู่พฤติกรรมดั้งเดิมของ dsh (เครื่องมือจัดการไฟล์ใช้พาธ Windows) bash เป็น Git Bash เสมอเมื่อติดตั้งปลั๊กอินนี้ สวิตช์นี้ควบคุมเฉพาะรูปแบบพาธระหว่างเครื่องมือเท่านั้น",
256
+ },
257
+ /* locale: tr */
258
+ "tr": {
259
+ "title": "Git Bash yol lehçesi",
260
+ "cardDesc": "Tüm araçlar için tek /c/ POSIX yol biçimi (varsayılan olarak açık)",
261
+ "state.label": "Geçerli durum",
262
+ "state.on": "Etkin",
263
+ "state.off": "Devre dışı",
264
+ "switch.on": "Etkinleştir",
265
+ "switch.off": "Devre dışı bırak",
266
+ "error": "yazma başarısız",
267
+ "hint": "Etkinleştirildiğinde: tüm araçlar (bash komutları ve workdir ile read/write/edit/read_image/glob/grep yol bağımsız değişkenleri) MSYS sürücü kökünden POSIX yolları (/c/Users/...) kullanır; ana süreç dosya araçlarının yol alanlarını otomatik olarak çevirir. Devre dışı bırakıldığında dsh yerel davranışına döner (dosya araçları için Windows yolları). Eklenti kurulu olduğu sürece bash her zaman Git Bash olarak kalır; bu anahtar yalnızca araçlar arası yol lehçesini yönetir.",
268
+ },
269
+ /* locale: vi */
270
+ "vi": {
271
+ "title": "Phương ngữ đường dẫn Git Bash",
272
+ "cardDesc": "Một dạng đường dẫn POSIX /c/ thống nhất cho mọi công cụ (mặc định bật)",
273
+ "state.label": "Trạng thái hiện tại",
274
+ "state.on": "Đã bật",
275
+ "state.off": "Đã tắt",
276
+ "switch.on": "Bật",
277
+ "switch.off": "Tắt",
278
+ "error": "ghi thất bại",
279
+ "hint": "Khi bật: mọi công cụ (lệnh bash và workdir, tham số đường dẫn của read/write/edit/read_image/glob/grep) dùng đường dẫn POSIX từ gốc ổ đĩa MSYS (/c/Users/...); máy chủ tự động chuyển đổi trường đường dẫn cho các công cụ tệp. Khi tắt, hành vi gốc của dsh được khôi phục (công cụ tệp dùng đường dẫn Windows). bash luôn là Git Bash khi plugin được cài đặt; công tắc này chỉ điều khiển phương ngữ đường dẫn giữa các công cụ.",
280
+ },
281
+ /* locale: zh-hk */
282
+ "zh-hk": {
283
+ "title": "Git Bash 路徑方言",
284
+ "cardDesc": "所有工具統一用 /c/ POSIX 路徑格式(預設開啟)",
285
+ "state.label": "目前狀態",
286
+ "state.on": "已啟用",
287
+ "state.off": "已關閉",
288
+ "switch.on": "啟用",
289
+ "switch.off": "停用",
290
+ "error": "寫入失敗",
291
+ "hint": "啟用後:所有工具(bash 指令同 workdir、read/write/edit/read_image/glob/grep 嘅路徑參數)統一使用 MSYS 磁碟根 POSIX 路徑(/c/Users/...);檔案工具嘅路徑參數由宿主自動翻譯,模型無感。停用後回復 dsh 原生行為(檔案工具用 Windows 路徑)。bash 永遠係 Git Bash,唔受呢個開關影響。",
292
+ },
293
+ /* locale: zh-mo */
294
+ "zh-mo": {
295
+ "title": "Git Bash 路徑方言",
296
+ "cardDesc": "所有工具統一用 /c/ POSIX 路徑格式(預設開啟)",
297
+ "state.label": "目前狀態",
298
+ "state.on": "已啟用",
299
+ "state.off": "已關閉",
300
+ "switch.on": "啟用",
301
+ "switch.off": "停用",
302
+ "error": "寫入失敗",
303
+ "hint": "啟用後:所有工具(bash 指令同 workdir、read/write/edit/read_image/glob/grep 嘅路徑參數)統一使用 MSYS 磁碟根 POSIX 路徑(/c/Users/...);檔案工具嘅路徑參數由宿主自動翻譯,模型無感。停用後回復 dsh 原生行為(檔案工具用 Windows 路徑)。bash 永遠係 Git Bash,唔受呢個開關影響。",
304
+ },
305
+ /* locale: zh-tw */
306
+ "zh-tw": {
307
+ "title": "Git Bash 路徑方言",
308
+ "cardDesc": "所有工具統一採用 /c/ POSIX 路徑格式(預設開啟)",
309
+ "state.label": "目前狀態",
310
+ "state.on": "已啟用",
311
+ "state.off": "已關閉",
312
+ "switch.on": "啟用",
313
+ "switch.off": "停用",
314
+ "error": "寫入失敗",
315
+ "hint": "啟用後:所有工具(bash 指令與 workdir,以及 read/write/edit/read_image/glob/grep 的路徑參數)統一使用 MSYS 磁碟根 POSIX 路徑(/c/Users/...);檔案工具的路徑參數由宿主自動轉譯,模型無感。停用後恢復 dsh 原生行為(檔案工具使用 Windows 路徑)。bash 一律是 Git Bash,不受此開關影響。",
316
+ },
317
+ };
318
+
319
+ // ── locale resolution (live: read per lookup, cached per tag) ───────────
320
+
321
+ function hasOwnKey(bag, key) {
322
+ return Object.prototype.hasOwnProperty.call(bag, key);
323
+ }
324
+
325
+ /* Resolve one locale tag against what this plugin ships: the exact tag,
326
+ then the primary subtag, then English. Chinese is special-cased because
327
+ the traditional variants are shipped while a bare zh-Hant-* is not
328
+ spelled out. */
329
+ function dictionaryFor(active) {
330
+ var raw = active === undefined || active === null || active === "" ? "en" : String(active);
331
+ var tag = raw.toLowerCase().replace(/_/g, "-");
332
+ var primary = tag.split("-")[0];
333
+ if (primary === "zh") {
334
+ if (hasOwnKey(LOCALES, tag)) return LOCALES[tag];
335
+ if (tag.indexOf("hant") >= 0 || tag === "zh-hk" || tag === "zh-mo" || tag === "zh-tw") {
336
+ return hasOwnKey(LOCALES, "zh-hk") ? LOCALES["zh-hk"] : zh;
337
+ }
338
+ return zh;
339
+ }
340
+ if (hasOwnKey(LOCALES, primary)) return LOCALES[primary];
341
+ return en;
342
+ }
343
+
344
+ /* The active locale tag, read fresh every time: DSH's language preference
345
+ switches live, so a value captured once at activation would keep answering
346
+ in the language that happened to be active when the card loaded. The
347
+ host-backed preference wins; the browser is the fallback. */
348
+ function activeLocaleOf(ctx) {
349
+ var active = "";
350
+ try {
351
+ var locale = ctx.get("locale");
352
+ if (locale !== undefined && typeof locale.getSnapshot === "function") {
353
+ var snapshot = locale.getSnapshot();
354
+ if (snapshot !== null && typeof snapshot === "object" && typeof snapshot.active === "string") active = snapshot.active;
355
+ }
356
+ } catch (error) { /* fall through to the browser */ }
357
+ if (active === "" && typeof navigator === "object" && navigator !== null && typeof navigator.language === "string") active = navigator.language;
358
+ return active;
359
+ }
360
+
361
+ /* A live lookup: the dictionary is picked per call and cached under the tag
362
+ it was picked for, so a language switch costs one string compare per
363
+ string. Falling back to EN — kept key-complete on purpose — means an
364
+ unsupported language shows English, never a raw key. */
365
+ function translatorOf(ctx) {
366
+ var cachedTag = null;
367
+ var cachedDict = null;
368
+ return function (key) {
369
+ var tag = activeLocaleOf(ctx);
370
+ if (tag !== cachedTag) { cachedTag = tag; cachedDict = dictionaryFor(tag); }
371
+ if (hasOwnKey(cachedDict, key)) return cachedDict[key];
372
+ return hasOwnKey(en, key) ? en[key] : key;
373
+ };
374
+ }
375
+
71
376
  // ── styles (gb- prefixed; tokens mirror PluginCard.module.css) ─────────
72
377
 
73
378
  var STYLE_ID = "dsh-gitbash-shell-style";
@@ -140,13 +445,40 @@ window.__ModuleLoader__.load({
140
445
  return this.props.children;
141
446
  };
142
447
 
448
+ // ── live locale (repaint the card on a language switch) ─────────────────
449
+
450
+ /**
451
+ * Wraps the card so a language switch repaints it instead of waiting for a
452
+ * page load. DSH's own components re-render on the locale snapshot, but this
453
+ * card belongs to a plugin: it subscribes for itself, and it hands the card
454
+ * THIS plugin's live lookup (translatorOf) — whose resolution covers the
455
+ * third languages the DSH catalog does not carry. The card stays inside the
456
+ * same quiet boundary as before.
457
+ */
458
+ function LocaleLive(props) {
459
+ var tickState = useState(0);
460
+ var tick = tickState[0];
461
+ var setTick = tickState[1];
462
+ void tick;
463
+
464
+ useEffect(function () {
465
+ var locale = props.ctx && typeof props.ctx.get === "function" ? props.ctx.get("locale") : undefined;
466
+ if (locale === undefined || typeof locale.subscribe !== "function") return undefined;
467
+ var unsubscribe = locale.subscribe(function () { setTick(function (value) { return value + 1; }); });
468
+ return typeof unsubscribe === "function" ? unsubscribe : undefined;
469
+ }, []);
470
+
471
+ return E(QuietBoundary, null, E(GitBashCard, Object.assign({}, props.cardProps, { t: props.t })));
472
+ }
473
+
143
474
  // ── settings card (module-level component) ───────────────────────────────
144
475
 
145
476
  /**
146
- * The Settings → Plugins card. t arrives through the registration's
147
- * locale field; scope arrives as a PLAIN prop from the inject factory.
148
- * Snapshots are read per render; each write bumps a local tick so the
149
- * card re-reads without an external-store hook adapter.
477
+ * The Settings → Plugins card. t arrives from LocaleLive (the plugin's own
478
+ * live lookup, keyed on the active locale at call time); scope arrives as a
479
+ * PLAIN prop from the inject factory. Snapshots are read per render; each
480
+ * write bumps a local tick so the card re-reads without an external-store
481
+ * hook adapter.
150
482
  */
151
483
  function GitBashCard(props) {
152
484
  var t = typeof props.t === "function" ? props.t : function (key) { return key; };
@@ -235,11 +567,17 @@ window.__ModuleLoader__.load({
235
567
 
236
568
  exports.apply = function (ctx) {
237
569
  var scope = ctx.settingsScope.bind({ namespace: SETTINGS_NAMESPACE });
570
+ /* The card's only copy entry point: a live lookup, never a captured
571
+ dictionary — the language preference switches without a reload. */
572
+ var t = translatorOf(ctx);
238
573
 
239
574
  ctx.effect(function () {
240
575
  var disposers = [ensureStyles()];
241
576
  try {
242
- var disposeDict = ctx.locale.register(NS, { zh: zh, en: en });
577
+ /* Every shipped dictionary rides the DSH locale registry, so host-side
578
+ consumers read the same copy the card does; a missing service simply
579
+ means the card is the only reader. */
580
+ var disposeDict = ctx.locale.register(NS, Object.assign({ zh: zh, en: en }, LOCALES));
243
581
  if (typeof disposeDict === "function") disposers.push(disposeDict);
244
582
  } catch (error) {
245
583
  console.warn(TAG + " dictionary registration failed:", error && error.message ? error.message : error);
@@ -270,8 +608,11 @@ window.__ModuleLoader__.load({
270
608
  // props: the bound settings scope rides here as a PLAIN member
271
609
  // (top-level options fields do NOT reach the component).
272
610
  inject: function () { return { scope: scope }; },
273
- }, function CardWithBoundary(props) {
274
- return E(QuietBoundary, null, E(GitBashCard, props));
611
+ }, function CardWithLocale(props) {
612
+ /* The registration's locale field also hands out the framework's own
613
+ t seat; the plugin's lookup wins (21 locale tags against the catalog's
614
+ zh/en), passed last so no prop merge order can override it. */
615
+ return E(LocaleLive, { ctx: ctx, t: t, cardProps: props });
275
616
  });
276
617
  });
277
618
  } catch (error) {
package/src/index.js CHANGED
@@ -231,6 +231,183 @@ async function detectPresentSupport(agentPresets) {
231
231
  }
232
232
  }
233
233
 
234
+ /** Row ids the shipped workflow-engine row has answered to (dsh 0.1.6 renamed it). */
235
+ const ENGINE_ROW_IDS = ['workflow-worker-thread', 'workflow-ptc']
236
+
237
+ /** Which built-in preset each of this plugin's variants mirrors, for row-form lookup. */
238
+ export const ROW_SOURCE = {
239
+ 'standard-gitbash': 'standard',
240
+ 'cordis-gitbash': 'cordis',
241
+ 'code-gitbash': 'ptc',
242
+ }
243
+
244
+ /** Leading whitespace of one line (pure). */
245
+ function indentOf(line) {
246
+ return line.slice(0, line.length - line.trimStart().length)
247
+ }
248
+
249
+ /** The unquoted value of a name line, or undefined when the line is not one (pure). */
250
+ function quotedName(line) {
251
+ const trimmed = line.trim()
252
+ if (!trimmed.startsWith('name: ')) return undefined
253
+ const raw = trimmed.slice(6).trim()
254
+ if (raw.length > 1 && raw.charAt(0) === "'" && raw.charAt(raw.length - 1) === "'") return raw.slice(1, -1)
255
+ return raw
256
+ }
257
+
258
+ /**
259
+ * Read ONE row's spelling out of a composition text (pure): the name value
260
+ * that follows its id line and whether the row's own block turns it off before
261
+ * the next row starts. Returns undefined when the row is absent, and never
262
+ * throws, so a composition that legitimately lacks the row (minimal) is simply
263
+ * left alone.
264
+ * @param text - composition text.
265
+ * @param rowId - exact id value to locate.
266
+ * @returns the row's spelling, or undefined.
267
+ */
268
+ export function rowFormOf(text, rowId) {
269
+ const lines = text.split('\n')
270
+ for (let i = 0; i < lines.length; i += 1) {
271
+ if (lines[i].trim() !== '- id: ' + rowId) continue
272
+ const nameLine = i + 1 < lines.length ? lines[i + 1] : ''
273
+ const name = quotedName(nameLine)
274
+ if (name === undefined) continue
275
+ let disabled = false
276
+ let disabledIndent = indentOf(nameLine)
277
+ for (let j = i + 2; j < lines.length; j += 1) {
278
+ if (lines[j].trim().startsWith('- id: ')) break
279
+ if (lines[j].trim() === 'disabled: true') {
280
+ disabled = true
281
+ disabledIndent = indentOf(lines[j])
282
+ break
283
+ }
284
+ }
285
+ return { id: rowId, name, indent: indentOf(lines[i]), nameIndent: indentOf(nameLine), disabled, disabledIndent }
286
+ }
287
+ return undefined
288
+ }
289
+
290
+ /**
291
+ * Read the two rows this plugin's committed compositions must track out of a
292
+ * SHIPPED composition text (pure). The built-in preset is the authority: it
293
+ * mounts on this host by construction, so whatever it spells is mountable
294
+ * here, on every era, without shipping one asset variant per rename.
295
+ * @param text - a shipped agent.cordis.yml.
296
+ * @returns an object with the engine row and the tool-ralph row; either may be undefined.
297
+ */
298
+ export function rowFormsOf(text) {
299
+ let engine
300
+ for (const id of ENGINE_ROW_IDS) {
301
+ engine = rowFormOf(text, id)
302
+ if (engine !== undefined) break
303
+ }
304
+ return { engine, ralph: rowFormOf(text, 'tool-ralph') }
305
+ }
306
+
307
+ /**
308
+ * Align the workflow-engine row with the host's own spelling (pure,
309
+ * idempotent, plain string surgery — never a YAML round-trip, so !!js literals
310
+ * survive).
311
+ *
312
+ * dsh 0.1.6-alpha.1 renamed this row workflow-worker-thread to workflow-ptc
313
+ * AND deleted the old package. A composition row whose module fails to import
314
+ * rejects the WHOLE preset mount (agent-presets mount.ts), so a committed old
315
+ * spelling breaks every Git Bash preset on the new host while the new spelling
316
+ * breaks them on hosts that still ship only the old package. The materializer
317
+ * therefore copies the row out of the host's own built-in preset instead of
318
+ * pinning either spelling.
319
+ * @param text - composition text.
320
+ * @param form - the host's engine row from rowFormsOf, or undefined to leave the text alone.
321
+ * @param options - engineEnabled forces the row on regardless of the host's own state.
322
+ * @returns the aligned text.
323
+ */
324
+ export function alignEngineRow(text, form, options) {
325
+ if (!form) return text
326
+ const engineEnabled = Boolean(options && options.engineEnabled)
327
+ let current
328
+ for (const id of ENGINE_ROW_IDS) {
329
+ current = rowFormOf(text, id)
330
+ if (current !== undefined) break
331
+ }
332
+ if (current === undefined) return text
333
+ const head = '- id: ' + current.id + '\n' + current.nameIndent + "name: '" + current.name + "'"
334
+ const wanted = '- id: ' + form.id + '\n' + current.nameIndent + "name: '" + form.name + "'"
335
+ const wantOff = engineEnabled ? false : form.disabled
336
+ let out = text.replace(head, wanted)
337
+ if (wantOff && !current.disabled) {
338
+ out = out.replace(wanted + '\n', wanted + '\n' + current.nameIndent + 'disabled: true\n')
339
+ } else if (!wantOff && current.disabled) {
340
+ out = out.replace(wanted + '\n' + current.disabledIndent + 'disabled: true\n', wanted + '\n')
341
+ }
342
+ return out
343
+ }
344
+
345
+ /**
346
+ * Align the tool-ralph row with the host's own default (pure, idempotent).
347
+ * dsh 0.1.6-alpha.1 ships ralph disabled in every built-in preset: its tool
348
+ * description restricts it to runs the human explicitly asked for. The row
349
+ * still imports on older hosts, so this is a semantics alignment rather than a
350
+ * mount fix — but a composition claiming to mirror the built-in preset should
351
+ * not silently re-enable a tool the deployment turned off.
352
+ * @param text - composition text.
353
+ * @param form - the host's tool-ralph row from rowFormsOf, or undefined.
354
+ * @returns the aligned text.
355
+ */
356
+ export function alignRalphRow(text, form) {
357
+ if (!form) return text
358
+ const current = rowFormOf(text, 'tool-ralph')
359
+ if (current === undefined) return text
360
+ const head = '- id: tool-ralph\n' + current.nameIndent + "name: '" + current.name + "'"
361
+ if (form.disabled && !current.disabled) {
362
+ return text.replace(head + '\n', head + '\n' + current.nameIndent + 'disabled: true\n')
363
+ }
364
+ if (!form.disabled && current.disabled) {
365
+ return text.replace(head + '\n' + current.disabledIndent + 'disabled: true\n', head + '\n')
366
+ }
367
+ return text
368
+ }
369
+
370
+ /**
371
+ * Probe every built-in preset this plugin's variants mirror for those row
372
+ * spellings (never throws). The live roster is the authority — package
373
+ * resolution alone would lie on linked development trees and on CLI installs
374
+ * whose first-party packages live outside the profile.
375
+ * @param agentPresets - the roster service.
376
+ * @returns a map of row forms per available built-in id, or undefined.
377
+ */
378
+ async function detectRowForms(agentPresets) {
379
+ try {
380
+ const list = await agentPresets.list()
381
+ const entries = Array.isArray(list) ? list : []
382
+ const out = {}
383
+ for (const id of ['ptc', 'standard', 'cordis']) {
384
+ const entry = entries.find((p) => p && p.id === id && typeof p.path === 'string')
385
+ if (!entry) continue
386
+ const file = /\.yml$/.test(entry.path) ? entry.path : join(entry.path, 'agent.cordis.yml')
387
+ out[id] = rowFormsOf(readFileSync(file, 'utf8'))
388
+ }
389
+ return Object.keys(out).length > 0 ? out : undefined
390
+ } catch (error) {
391
+ console.log(TAG + ' row-form probe failed (' + (error && error.message ? error.message : error) + ') — keeping the committed spellings')
392
+ return undefined
393
+ }
394
+ }
395
+
396
+ /**
397
+ * Marker fingerprint of the row spellings one variant is aligned to, so a host
398
+ * that renames (or re-defaults) them re-materializes on the next boot.
399
+ * @param rows - detectRowForms result.
400
+ * @param presetId - the variant being materialized.
401
+ * @returns a short stable string, '' when nothing was probed.
402
+ */
403
+ function rowFingerprint(rows, presetId) {
404
+ const source = ROW_SOURCE[presetId]
405
+ const form = rows && source ? rows[source] : undefined
406
+ if (!form || !form.engine) return ''
407
+ const ralph = form.ralph === undefined ? 'na' : (form.ralph.disabled ? 'off' : 'on')
408
+ return form.engine.id + ':' + (form.engine.disabled ? 'off' : 'on') + ':' + ralph
409
+ }
410
+
234
411
  /**
235
412
  * Pick the committed composition asset inside one variant directory (pure).
236
413
  * The ptc-era twin is preferred when the roster says `ptc`; the persona-split
@@ -250,11 +427,17 @@ function pickComposition(base, persona, available) {
250
427
  }
251
428
 
252
429
  /** Startup decision for an existing unmodified tree: 'refresh' or 'idle'. */
253
- function syncDecision({ state, marker, version, sourceHashes, base = 'code', persona = 'text', present = false }) {
430
+ function syncDecision({ state, marker, version, sourceHashes, base = 'code', persona = 'text', present = false, rows = '' }) {
254
431
  if (state !== 'unmodified' || !marker) return 'refresh'
255
432
  if (marker.version !== version) return 'refresh'
256
433
  if (marker.base !== base) return 'refresh'
257
434
  if ((marker.persona ?? 'text') !== persona) return 'refresh'
435
+ // Host row-form flip: dsh 0.1.6-alpha.1 renamed the workflow-engine row and
436
+ // deleted the package behind the old spelling. A composition row that cannot
437
+ // be imported rejects the WHOLE preset mount, so a host reporting a
438
+ // different spelling must re-materialize (same marker-dimension pattern as
439
+ // persona and present).
440
+ if ((marker.rows ?? '') !== rows) return 'refresh'
258
441
  // Host-capability flip: @deepseek-ai/dsh-tool-present first shipped with dsh
259
442
  // 0.1.5-alpha.2. A preset composed with the row on a host that lacks the
260
443
  // package REJECTS THE WHOLE MOUNT (mount.ts: a row whose module failed to
@@ -315,7 +498,7 @@ async function hostHasToolPresent() {
315
498
  }
316
499
 
317
500
  /** Write one preset directory from scratch. Returns 'ok' or 'no-skills-source'. */
318
- function materialize({ target, presetId, skillsSource, version, base = 'code', persona = 'text', present = false }) {
501
+ function materialize({ target, presetId, skillsSource, version, base = 'code', persona = 'text', present = false, rows }) {
319
502
  rmSync(target, { recursive: true, force: true })
320
503
  mkdirSync(target, { recursive: true })
321
504
 
@@ -323,6 +506,17 @@ function materialize({ target, presetId, skillsSource, version, base = 'code', p
323
506
  const compositionFile = pickComposition(base, persona, available)
324
507
  let composition = readFileSync(join(pkgDir, 'assets', presetId, compositionFile), 'utf8')
325
508
  if (present && compositionFile.includes('.ptc.')) composition = injectPresentRow(composition)
509
+ // Row-form alignment (v0.14.0): the committed assets pin one spelling of the
510
+ // workflow-engine row, but a host renames it out from under them (dsh
511
+ // 0.1.6-alpha.1) and a row whose module fails to import rejects the whole
512
+ // mount. Copy the host's own spelling rather than shipping a third asset
513
+ // variant per era. Absent probe = leave the frozen history untouched.
514
+ const rowSource = ROW_SOURCE[presetId]
515
+ const rowForm = rows && rowSource ? rows[rowSource] : undefined
516
+ if (rowForm) {
517
+ composition = alignEngineRow(composition, rowForm.engine)
518
+ composition = alignRalphRow(composition, rowForm.ralph)
519
+ }
326
520
  writeFileSync(join(target, 'agent.cordis.yml'), composition)
327
521
  writeFileSync(join(target, 'preset.yml'), readFileSync(join(pkgDir, 'assets', presetId, 'preset.yml')))
328
522
 
@@ -337,7 +531,7 @@ function materialize({ target, presetId, skillsSource, version, base = 'code', p
337
531
  }
338
532
  }
339
533
 
340
- const marker = { managedBy: MANAGED_BY, version, presetId, base, persona, present, files: hashTree(target) }
534
+ const marker = { managedBy: MANAGED_BY, version, presetId, base, persona, present, rows: rowFingerprint(rows, presetId), files: hashTree(target) }
341
535
  writeFileSync(join(target, MARKER_FILE), JSON.stringify(marker, null, 2) + '\n')
342
536
  return skills
343
537
  }
@@ -729,7 +923,7 @@ export async function apply(ctx, config = {}) {
729
923
  const disposeGitBash = ctx.provide('gitBash', gitBashCapability)
730
924
  ctx.effect(() => disposeGitBash, 'dsh-gitbash-shell: gitBash capability')
731
925
 
732
- // ── settings namespace: the posixPaths switch (default OFF) ──────────────
926
+ // ── settings namespace: the posixPaths switch (default ON, v0.10.0) ──────
733
927
  // Served on the host so the Plugins tab pairs it with the browser card
734
928
  // (the tab dispatches Host-served namespaces ∩ registered cards). Dynamic
735
929
  // imports keep the zero-dependency smoke path importable; the schema MUST
@@ -951,6 +1145,10 @@ export async function apply(ctx, config = {}) {
951
1145
  // resolving from this plugin's own tree (covers hosts where the roster
952
1146
  // probe is unavailable).
953
1147
  const present = (await detectPresentSupport(ctx.agentPresets)) || (await hostHasToolPresent())
1148
+ // Row spellings of the built-in presets these variants mirror: the
1149
+ // workflow-engine row was renamed and its old package deleted in dsh
1150
+ // 0.1.6-alpha.1, so the materializer copies whatever the host itself ships.
1151
+ const rows = await detectRowForms(ctx.agentPresets)
954
1152
  const userRootPath = userRoot.path
955
1153
  purgeOrphans(userRootPath, presetIds)
956
1154
 
@@ -980,12 +1178,13 @@ export async function apply(ctx, config = {}) {
980
1178
 
981
1179
  const sourceHashes = presetId === 'cordis-gitbash' ? skillsHashes(skillsSource) : null
982
1180
  const marker = readMarker(target)
983
- if (state === 'unmodified' && syncDecision({ state, marker, version, sourceHashes, base, persona, present }) === 'idle') {
1181
+ const markerRows = rowFingerprint(rows, presetId)
1182
+ if (state === 'unmodified' && syncDecision({ state, marker, version, sourceHashes, base, persona, present, rows: markerRows }) === 'idle') {
984
1183
  ctx.logger?.('gitbash-shell')?.debug?.( `preset '${presetId}' up to date (v${version}, ${base}-era${persona === 'split' ? ', persona-split' : ''}) — idle`)
985
1184
  continue
986
1185
  }
987
1186
 
988
- const skills = materialize({ target, presetId, skillsSource, version, base, persona, present })
1187
+ const skills = materialize({ target, presetId, skillsSource, version, base, persona, present, rows })
989
1188
  const verb = state === 'absent' ? 'materialized' : 'refreshed'
990
1189
  console.log(
991
1190
  `${TAG} ${verb} preset '${presetId}' into ${userRootPath} (v${version}, ${base}-era composition${persona === 'split' ? ', persona-split' : ''})` +
@@ -995,4 +1194,4 @@ export async function apply(ctx, config = {}) {
995
1194
  }
996
1195
 
997
1196
  // Test surface: pure helpers, no Cordis context required.
998
- export const _internal = { PRESET_IDS, translateMsysPath, translatePathArguments, readPosixPaths, windowsToMsys, rewriteResultPaths, MARKER_FILE, classify, materialize, cleanupOnDispose, firstUserRoot, hashTree, skillsHashes, syncDecision, installRegisterShim, baseForRoster, detectBase, pickComposition, personaEraForText, detectPersonaEra, injectPresentRow, hostHasToolPresent, detectPresentSupport }
1197
+ export const _internal = { PRESET_IDS, translateMsysPath, translatePathArguments, readPosixPaths, windowsToMsys, rewriteResultPaths, MARKER_FILE, classify, materialize, cleanupOnDispose, firstUserRoot, hashTree, skillsHashes, syncDecision, installRegisterShim, baseForRoster, detectBase, pickComposition, personaEraForText, detectPersonaEra, injectPresentRow, hostHasToolPresent, detectPresentSupport, rowFormOf, rowFormsOf, alignEngineRow, alignRalphRow, ROW_SOURCE }
package/src/shell.js CHANGED
@@ -70,12 +70,52 @@ export class GitBashSandboxExecutor extends SandboxBashExecutor {
70
70
  /**
71
71
  * Wrap one shell command via the ctx.sandbox provider, substituting Git Bash
72
72
  * for the shipped bare `bash` argv.
73
+ *
74
+ * Signature spans both dsh eras: 0.1.6-alpha.1 made the provider's confine
75
+ * async and handed it the caller's abort signal (the base class now calls
76
+ * `this.confine(command, policy, signal)` and awaits it). The predecessor
77
+ * was synchronous and took two arguments. Forwarding the third argument is
78
+ * safe on both — the old provider ignores it and its synchronous return is
79
+ * what the old base class still consumes — while dropping it on the new host
80
+ * would silently detach sandbox preparation from cancellation.
73
81
  * @param command - shell source for the confined inner `bash -c`.
74
82
  * @param policy - resolved confined execution policy.
75
- * @returns the provider's exact argv and settlement-classification facts.
83
+ * @param signal - caller deadline/cancellation, forwarded when the host provides one.
84
+ * @returns the provider's exact argv and settlement-classification facts (a promise on dsh >= 0.1.6).
76
85
  */
77
- confine(command, policy) {
78
- return this.ctx.sandbox.confine([this.bashPath, '-c', command], policy)
86
+ confine(command, policy, signal) {
87
+ return this.ctx.sandbox.confine([this.bashPath, '-c', command], policy, signal)
88
+ }
89
+
90
+ /**
91
+ * Whether the LOADED base class declares an async `start`, probed once
92
+ * (pure). dsh 0.1.6-alpha.1 turned LocalBashExecutor.start into an async
93
+ * method returning Promise<ShellProcess>; before that it returned the live
94
+ * handle directly. This override has to match whichever contract the host
95
+ * actually carries: handing a plain handle to a host that awaits is fine,
96
+ * but handing a Promise to a synchronous host would give it a thenable
97
+ * instead of a process. Shape is probed, never inferred from a version.
98
+ * @returns true when the base class start is an async function.
99
+ */
100
+ static baseStartIsAsync() {
101
+ return SandboxBashExecutor.prototype.start.constructor.name === 'AsyncFunction'
102
+ }
103
+
104
+ /**
105
+ * Unwrap `runArgv`'s settlement across dsh eras (pure). 0.1.6-alpha.1
106
+ * changed the protected hook's return value from the bare ShellRunResult to
107
+ * `{ result, spawnRequested }`, where spawnRequested is false when
108
+ * preparation was cancelled before any spawn. Spreading the wrapper as if it
109
+ * were the result would hand the caller an object carrying no exitCode and
110
+ * no streams at all, so it is unwrapped by shape — never by version.
111
+ * @param raw - whatever the loaded `runArgv` resolved to.
112
+ * @returns the bare result plus whether argv ever reached the provider.
113
+ */
114
+ static unwrapRunArgv(raw) {
115
+ if (raw !== null && typeof raw === 'object' && typeof raw.spawnRequested === 'boolean' && raw.result !== undefined) {
116
+ return { result: raw.result, spawnRequested: raw.spawnRequested }
117
+ }
118
+ return { result: raw, spawnRequested: true }
79
119
  }
80
120
 
81
121
  /**
@@ -93,13 +133,17 @@ export class GitBashSandboxExecutor extends SandboxBashExecutor {
93
133
  if (policy === undefined) return super.run(spec)
94
134
  const { mode } = policy
95
135
  if (mode === 'danger-full-access') {
96
- const result = await this.runArgv(spec, [this.bashPath, '-c', spec.command])
136
+ const { result } = GitBashSandboxExecutor.unwrapRunArgv(
137
+ await this.runArgv(spec, [this.bashPath, '-c', spec.command]))
97
138
  return { ...result, sandbox: { mode, denied: false } }
98
139
  }
99
140
  if (process.platform === 'win32') {
100
141
  this.warnConfinedUnconfined(mode)
101
- const result = await this.runArgv(spec, [this.bashPath, '-c', spec.command])
102
- return { ...result, sandbox: { mode, denied: false, enforcement: 'unconfined' } }
142
+ const { result, spawnRequested } = GitBashSandboxExecutor.unwrapRunArgv(
143
+ await this.runArgv(spec, [this.bashPath, '-c', spec.command]))
144
+ // No spawn means no argv ran, so there is nothing to label as
145
+ // unconfined; the parent reports the same bare sandbox facts there.
146
+ return { ...result, sandbox: spawnRequested ? { mode, denied: false, enforcement: 'unconfined' } : { mode, denied: false } }
103
147
  }
104
148
  return super.run(spec)
105
149
  }
@@ -108,16 +152,14 @@ export class GitBashSandboxExecutor extends SandboxBashExecutor {
108
152
  const policy = spec.sandboxPolicy
109
153
  if (policy === undefined) return super.start(spec)
110
154
  const { mode } = policy
111
- if (mode === 'danger-full-access') {
112
- const proc = this.startArgv(spec, [this.bashPath, '-c', spec.command])
113
- proc.sandbox = { mode, denied: false }
114
- return proc
115
- }
116
- if (process.platform === 'win32') {
117
- this.warnConfinedUnconfined(mode)
155
+ const fullAccess = mode === 'danger-full-access'
156
+ if (fullAccess || process.platform === 'win32') {
157
+ if (!fullAccess) this.warnConfinedUnconfined(mode)
118
158
  const proc = this.startArgv(spec, [this.bashPath, '-c', spec.command])
119
- proc.sandbox = { mode, denied: false, enforcement: 'unconfined' }
120
- return proc
159
+ proc.sandbox = fullAccess ? { mode, denied: false } : { mode, denied: false, enforcement: 'unconfined' }
160
+ // Match the loaded base contract: the async one (dsh >= 0.1.6) is
161
+ // awaited by its callers, the synchronous one is consumed directly.
162
+ return GitBashSandboxExecutor.baseStartIsAsync() ? Promise.resolve(proc) : proc
121
163
  }
122
164
  return super.start(spec)
123
165
  }