dsh-win-pwsh7 0.2.3 → 0.2.5
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 +26 -0
- package/README.en.md +42 -3
- package/README.md +34 -3
- package/client.js +120 -88
- package/index.js +10 -0
- package/lib/utf8-console-bridge.js +104 -0
- package/package.json +3 -2
- package/tools/check-client.mjs +26 -5
- package/tools/check-utf8-bridge.mjs +148 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.5 — transitional Windows UTF-8 console bridge
|
|
4
|
+
|
|
5
|
+
- Added `lib/utf8-console-bridge.js`, a removable layer that wraps the same `ctx.shell.execute`
|
|
6
|
+
the `pwsh` tool uses and prefixes Windows commands with `chcp.com 65001 > $null;`. A nested
|
|
7
|
+
`pwsh -File` / `powershell -File` otherwise inherits the console code page (936 on a Simplified
|
|
8
|
+
Chinese machine) and emits GBK, which the Host collector decodes as UTF-8 and renders as U+FFFD.
|
|
9
|
+
- `utf8Console` (default `true`) gates the layer. It fails open when the shell service, the
|
|
10
|
+
executor shape or the config reader is unavailable, and restores the original executor when the
|
|
11
|
+
plugin unloads. No tool, prompt, install or gate behavior changed.
|
|
12
|
+
- `tools/check-utf8-bridge.mjs` covers the prefix, opt-out, idempotence, restore and fail-open paths
|
|
13
|
+
with a fake executor; it runs in the `check` chain.
|
|
14
|
+
- Removal contract: delete `lib/utf8-console-bridge.js`, its import and mount in `index.js`, the
|
|
15
|
+
`utf8Console` config field, the bridge gate and its `check` entry, plus the README section and
|
|
16
|
+
this note. The README records the exact steps and the `grep` acceptance check.
|
|
17
|
+
|
|
18
|
+
## 0.2.4 — plugin page aligned with the dsh-ops card language
|
|
19
|
+
|
|
20
|
+
- Restyled both contributions after the installed dsh-ops 0.5.0 branch: a 0.5px hairline card on
|
|
21
|
+
`radius-lg` with a shaded card body, 13px/600 group titles, max-content fact labels, and the
|
|
22
|
+
same input, button and focus-token metrics.
|
|
23
|
+
- Split the configure surface into two cards: the executor path sits in a shaded body, the
|
|
24
|
+
dsh-ops coexistence switch sits in the card head, and save/reset follow the cards.
|
|
25
|
+
- The status section is one card now: title, refresh metric and action in the head, the facts in
|
|
26
|
+
a shaded body, and the profile patch as a sunken preview block.
|
|
27
|
+
- Updated the client gate to assert the card language instead of the retired flat section.
|
|
28
|
+
|
|
3
29
|
## 0.2.3 — removable dsh-ops coexistence bridge
|
|
4
30
|
|
|
5
31
|
- Moved the bridge, profile-patch ownership, and pure name planning into `lib/dsh-ops-bridge.js`.
|
package/README.en.md
CHANGED
|
@@ -44,7 +44,7 @@ multi-session validation passed, so the switch is default-on again.
|
|
|
44
44
|
## Install this plugin
|
|
45
45
|
|
|
46
46
|
```powershell
|
|
47
|
-
dsh plugin --profile desktop add github:XinSpark/dsh-win-pwsh7#v0.2.
|
|
47
|
+
dsh plugin --profile desktop add github:XinSpark/dsh-win-pwsh7#v0.2.5
|
|
48
48
|
# or
|
|
49
49
|
dsh plugin --profile desktop add dsh-win-pwsh7
|
|
50
50
|
```
|
|
@@ -83,6 +83,7 @@ The count lives in the current process and resets when DSH restarts.
|
|
|
83
83
|
|---|---|---|
|
|
84
84
|
| `overridePath` | unset | Priority ① explicit `pwsh.exe`. It must spawn and report PowerShell 7+; a failed validation falls back to auto-detection. **It only changes probing and recommendations, not executor wiring.** |
|
|
85
85
|
| `dshOpsCompat` | `true` | Coexist with dsh-ops (on by default): writes and enables its Compatibility mode, hides duplicate official tools, and keeps `pwsh` + `ops_bash`. Requires a restart. |
|
|
86
|
+
| `utf8Console` | `true` | Transitional: prefixes every `pwsh` tool command with `chcp.com 65001` on Windows, so a nested `pwsh -File` cannot emit GBK under code page 936 and be decoded as UTF-8. Removable once the Host ships the same fix. |
|
|
86
87
|
|
|
87
88
|
`overridePath` is `volatile()`: the plugin-page configuration section saves it live; it can also be
|
|
88
89
|
set in the profile patch:
|
|
@@ -129,6 +130,42 @@ The status section is a native card showing the state, runtime and executor path
|
|
|
129
130
|
at the top right; the configuration section exposes `overridePath` and the default-on "Keep native
|
|
130
131
|
pwsh alongside dsh-ops" native switch.
|
|
131
132
|
|
|
133
|
+
## Windows UTF-8 console (transitional)
|
|
134
|
+
|
|
135
|
+
The Host collector decodes `pwsh` output as UTF-8, but a `pwsh -File` / `powershell -File` started
|
|
136
|
+
inside a tool command inherits the **console code page** (936 on a Simplified Chinese machine) and
|
|
137
|
+
emits GBK, which then renders as `�`. The Host preamble pins the outer process's .NET encoding, not
|
|
138
|
+
the console code page.
|
|
139
|
+
|
|
140
|
+
0.2.5 adds a transitional layer: it wraps the same `ctx.shell.execute` the `pwsh` tool uses and
|
|
141
|
+
prefixes every Windows command with `chcp.com 65001 > $null;`. It affects only this plugin's users,
|
|
142
|
+
is on by default, can be disabled with `utf8Console: false`, and fails open when the Host shape
|
|
143
|
+
does not match.
|
|
144
|
+
|
|
145
|
+
### Removal contract
|
|
146
|
+
|
|
147
|
+
After the Host ships the same fix, delete the layer whole:
|
|
148
|
+
|
|
149
|
+
1. delete `lib/utf8-console-bridge.js`;
|
|
150
|
+
2. delete the import and `mountUtf8ConsoleBridge(ctx, current)` call in `index.js`;
|
|
151
|
+
3. delete `Config.utf8Console`;
|
|
152
|
+
4. delete `tools/check-utf8-bridge.mjs` and its entry in the `package.json` `check` chain;
|
|
153
|
+
5. delete this section and the 0.2.5 transitional note in CHANGELOG.
|
|
154
|
+
|
|
155
|
+
Acceptance: `grep -rn "utf8-console-bridge\|utf8Console" index.js lib tools` returns nothing.
|
|
156
|
+
|
|
157
|
+
### Deciding that the Host has fixed it
|
|
158
|
+
|
|
159
|
+
Either signal is enough to start removing the layer:
|
|
160
|
+
|
|
161
|
+
- `@deepseek-ai/dsh-pwsh-local`'s own `ENCODING_PREAMBLE` includes the `chcp.com 65001` switch
|
|
162
|
+
(check `packages/shell/pwsh-local/src/index.ts` upstream, or `lib/index.js` in the installed
|
|
163
|
+
package);
|
|
164
|
+
- with this plugin absent or `utf8Console` off, a nested `pwsh -File` under a CP936 console already
|
|
165
|
+
prints UTF-8.
|
|
166
|
+
|
|
167
|
+
Re-run the nested script in a real CP936 environment before deleting.
|
|
168
|
+
|
|
132
169
|
## Disable or uninstall
|
|
133
170
|
|
|
134
171
|
Disabling the plugin removes the `pwsh` gate and the plugin page; it does not uninstall PowerShell 7.
|
|
@@ -143,6 +180,7 @@ to 5.1, uninstall the Store build or pin the executor to `powershell.exe` (not r
|
|
|
143
180
|
| The Store page opens without an Install button | Confirm the VPN is off, fully quit and restart Microsoft Store, then retry; or use the manual path. |
|
|
144
181
|
| `pwsh` still runs 5.1 | Run `pwsh7_status`, paste the reported `pwshPath` patch into the profile, and restart; or install the Store build. |
|
|
145
182
|
| The plugin page is missing | Confirm `dsh-win-pwsh7` is enabled in the current profile, then restart DSH. |
|
|
183
|
+
| Chinese turns into `�` when a command nests `pwsh` | 0.2.5 handles it through `utf8Console`; if you disabled it, turn it back on or prefix the command with `chcp.com 65001 > $null;`. |
|
|
146
184
|
|
|
147
185
|
## Verification
|
|
148
186
|
|
|
@@ -150,10 +188,11 @@ to 5.1, uninstall the Store build or pin the executor to `powershell.exe` (not r
|
|
|
150
188
|
$env:DSH_APP_DIR = 'E:\FXS\APP\DSH NEXT\resources\app.asar'
|
|
151
189
|
node tools/check-host.mjs
|
|
152
190
|
node tools/check-client.mjs
|
|
191
|
+
node tools/check-utf8-bridge.mjs
|
|
153
192
|
node tools/check-native-contracts.mjs
|
|
154
193
|
```
|
|
155
194
|
|
|
156
|
-
All
|
|
195
|
+
All four gates run offline against a packed installation (`resources/app.asar`); `DSH_APP_DIR`
|
|
157
196
|
accepts the install root, `resources/app`, or `resources/app.asar`.
|
|
158
197
|
|
|
159
198
|
## Design constraints
|
|
@@ -163,7 +202,7 @@ accepts the install root, `resources/app`, or `resources/app.asar`.
|
|
|
163
202
|
- No VPN auto-detection and no metered-connection preflight: the user confirms the VPN before an
|
|
164
203
|
install, and a page-load failure asks again.
|
|
165
204
|
- No automatic retries; the three-failure breaker ends the loop.
|
|
166
|
-
- Run all
|
|
205
|
+
- Run all four gates before publishing; end-to-end installation is release evidence, not part of
|
|
167
206
|
the automated gate.
|
|
168
207
|
|
|
169
208
|
## License
|
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
|
|
37
37
|
```powershell
|
|
38
38
|
# 从 GitHub 安装(推荐锁定 tag)
|
|
39
|
-
dsh plugin --profile desktop add github:XinSpark/dsh-win-pwsh7#v0.2.
|
|
39
|
+
dsh plugin --profile desktop add github:XinSpark/dsh-win-pwsh7#v0.2.5
|
|
40
40
|
# 或 npm
|
|
41
41
|
dsh plugin --profile desktop add dsh-win-pwsh7
|
|
42
42
|
```
|
|
@@ -70,6 +70,7 @@ dsh plugin --profile desktop add dsh-win-pwsh7
|
|
|
70
70
|
|---|---|---|
|
|
71
71
|
| `overridePath` | 未设 | 优先级①的显式 `pwsh.exe`。必须能启动并报告 PowerShell 7+;未通过校验就回落到自动探测。**只影响探测与推荐,不接线**。 |
|
|
72
72
|
| `dshOpsCompat` | `true` | 与 dsh-ops 共存(默认开启):自动写入并启用兼容模式,隐藏重复的官方工具,保留 `pwsh` + `ops_bash`。需要重启。 |
|
|
73
|
+
| `utf8Console` | `true` | 过渡层:Windows 下给 `pwsh` 工具的每条命令前加 `chcp.com 65001`,避免嵌套 `pwsh -File` 按 936 输出 GBK 后被按 UTF-8 解码成乱码。宿主修复后可删除。 |
|
|
73
74
|
|
|
74
75
|
`overridePath` 是 `volatile()`:插件页配置区保存即生效;也可以写进 profile 补丁:
|
|
75
76
|
|
|
@@ -107,6 +108,34 @@ The executor resolves to the version-stable Store family alias: … Nothing to c
|
|
|
107
108
|
|
|
108
109
|
插件页的状态区是原生卡片,显示状态、运行时和执行器路径,右上角有「重新检测」;配置区有 `overridePath` 和默认开启的「与 dsh-ops 共存」原生开关。
|
|
109
110
|
|
|
111
|
+
## Windows UTF-8 控制台(过渡层)
|
|
112
|
+
|
|
113
|
+
DSH 的 `pwsh` 工具按 UTF-8 解码输出,但命令里再启动的 `pwsh -File` / `powershell -File` 会继承**控制台代码页**(简体中文机器通常是 936),输出 GBK,最终显示为 `�`。宿主的前缀只固定了外层进程的 .NET 编码,没有改控制台代码页。
|
|
114
|
+
|
|
115
|
+
0.2.5 增加一个过渡层:包装 `pwsh` 工具使用的 `ctx.shell.execute`,在 Windows 上给每条命令前加 `chcp.com 65001 > $null;`。它只影响装了本插件的用户;默认开启,可用 `utf8Console: false` 关闭;宿主接口形状不符时 fail-open,不影响 `pwsh` 工具本身。
|
|
116
|
+
|
|
117
|
+
### 删除契约
|
|
118
|
+
|
|
119
|
+
宿主发布同样的修复后,按这五步整块删除:
|
|
120
|
+
|
|
121
|
+
1. 删 `lib/utf8-console-bridge.js`;
|
|
122
|
+
2. 删 `index.js` 的 import 和 `mountUtf8ConsoleBridge(ctx, current)` 调用;
|
|
123
|
+
3. 删 `Config.utf8Console`;
|
|
124
|
+
4. 删 `tools/check-utf8-bridge.mjs`,并从 `package.json` 的 `check` 链里移除;
|
|
125
|
+
5. 删本节和 CHANGELOG 里的 0.2.5 过渡说明。
|
|
126
|
+
|
|
127
|
+
验收:`grep -rn "utf8-console-bridge\|utf8Console" index.js lib tools` 无结果。
|
|
128
|
+
|
|
129
|
+
### 判定上游已修复
|
|
130
|
+
|
|
131
|
+
满足任一条件即可开始删除:
|
|
132
|
+
|
|
133
|
+
- `@deepseek-ai/dsh-pwsh-local` 的 `ENCODING_PREAMBLE` 自己包含 `chcp.com 65001`(可查上游
|
|
134
|
+
`packages/shell/pwsh-local/src/index.ts`,或安装包里的 `lib/index.js`);
|
|
135
|
+
- 在 CP936 控制台下,不装/关闭本插件时,命令里嵌套的 `pwsh -File` 已经输出 UTF-8。
|
|
136
|
+
|
|
137
|
+
删除前先在真实的 CP936 环境复跑一遍嵌套脚本,确认不再需要这层。
|
|
138
|
+
|
|
110
139
|
## 禁用或卸载
|
|
111
140
|
|
|
112
141
|
禁用插件只移除 `pwsh` 的 gate 和插件页,不会卸载 PowerShell 7。只要机器上仍有 `pwsh.exe`,默认执行器仍会优先运行它(通常是 Store 版 PowerShell 7);只有找不到 `pwsh.exe` 时才回退到 Windows PowerShell 5.1。要真正回到 5.1,需要卸载 Store 版或把执行器 pin 到 `powershell.exe`,不推荐这样做。
|
|
@@ -118,6 +147,7 @@ The executor resolves to the version-stable Store family alias: … Nothing to c
|
|
|
118
147
|
| Store 页面打开但没有「安装」按钮 | 确认 VPN 已关闭,完全退出并重启 Microsoft Store 后重试;或改用手动安装。 |
|
|
119
148
|
| `pwsh` 仍运行 5.1 | 运行 `pwsh7_status`,按报告把 `pwshPath` 补丁粘进 profile 并重启;或安装 Store 版。 |
|
|
120
149
|
| 插件页看不到 | 确认当前 profile 已启用 `dsh-win-pwsh7`,然后重启 DSH。 |
|
|
150
|
+
| 命令里嵌套 `pwsh` 时中文变 `�` | 0.2.5 默认已由 `utf8Console` 过渡层处理;若手动关掉,重新开启,或在命令前加 `chcp.com 65001 > $null;`。 |
|
|
121
151
|
|
|
122
152
|
## 验证
|
|
123
153
|
|
|
@@ -125,17 +155,18 @@ The executor resolves to the version-stable Store family alias: … Nothing to c
|
|
|
125
155
|
$env:DSH_APP_DIR = 'E:\FXS\APP\DSH NEXT\resources\app.asar'
|
|
126
156
|
node tools/check-host.mjs
|
|
127
157
|
node tools/check-client.mjs
|
|
158
|
+
node tools/check-utf8-bridge.mjs
|
|
128
159
|
node tools/check-native-contracts.mjs
|
|
129
160
|
```
|
|
130
161
|
|
|
131
|
-
|
|
162
|
+
四套门都可以在打包安装(`resources/app.asar`)上离线复跑;`DSH_APP_DIR` 接安装根目录、`resources/app` 或 `resources/app.asar`。
|
|
132
163
|
|
|
133
164
|
## 设计约束
|
|
134
165
|
|
|
135
166
|
- 判定读执行器自己的 `ctx.shell.pwshPath`,不是扫配置行;`overridePath` 只影响探测与推荐。
|
|
136
167
|
- 不做 VPN 自动检测、不做计量连接预检;VPN 由用户在安装前确认,页面加载失败时再提示。
|
|
137
168
|
- 不自动重试;连续失败由熔断终止。
|
|
138
|
-
-
|
|
169
|
+
- 提交前跑四套门;端到端安装是发布证据,不是自动化门的一部分。
|
|
139
170
|
|
|
140
171
|
## License
|
|
141
172
|
|
package/client.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
// client.js — the browser half of dsh-win-pwsh7: the configure
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// client.js — the browser half of dsh-win-pwsh7: the configure cards and the status card
|
|
2
|
+
// on the plugin pages. Both use the same chrome as the installed dsh-ops branch — a 0.5px
|
|
3
|
+
// hairline card on radius-lg, a shaded card body, 13px/600 titles and max-content fact
|
|
4
|
+
// labels — while the Host page keeps its own 32px rhythm between detail sections.
|
|
4
5
|
//
|
|
5
6
|
// One operation, two callers: the diagnostics section and the re-detect button call the
|
|
6
7
|
// same Host method (`status`) that the `pwsh7_status` agent tool runs — over the Remote
|
|
@@ -97,6 +98,8 @@ window.__ModuleLoader__.load({
|
|
|
97
98
|
redetect: 'Re-detect',
|
|
98
99
|
redetecting: 'Detecting…',
|
|
99
100
|
card_title: 'Configuration',
|
|
101
|
+
executor_title: 'PowerShell 7 executor',
|
|
102
|
+
executor_hint: 'The runtime the pwsh tool runs. This page only probes and recommends; the profile patch in the status section does the wiring.',
|
|
100
103
|
field_overridePath: 'PowerShell 7 path',
|
|
101
104
|
overridePath_hint: 'Optional explicit pwsh.exe path (`overridePath`). Empty = auto-detect. It must spawn and report PowerShell 7 or newer; the Store family alias is accepted, while a version-numbered Store path is never pinned. This only changes what the plugin probes and recommends — it does not wire the pwsh tool. Use the patch in the status section for that.',
|
|
102
105
|
field_dshOpsCompat: 'Coexist with dsh-ops',
|
|
@@ -141,6 +144,8 @@ window.__ModuleLoader__.load({
|
|
|
141
144
|
redetect: '重新检测',
|
|
142
145
|
redetecting: '检测中…',
|
|
143
146
|
card_title: '配置',
|
|
147
|
+
executor_title: 'PowerShell 7 执行器',
|
|
148
|
+
executor_hint: 'pwsh 工具实际运行的 PowerShell。本页只做探测与推荐;接线用状态区里的 profile 补丁。',
|
|
144
149
|
field_overridePath: 'PowerShell 7 路径',
|
|
145
150
|
overridePath_hint: '可选的显式 pwsh.exe 路径(`overridePath`)。留空 = 自动探测。必须能启动并报告 PowerShell 7 及以上;Store 家族别名可用,版本号目录永不被 pin。这里只影响插件的探测与推荐,不会给 pwsh 工具接线;接线请用状态区里的补丁。',
|
|
146
151
|
field_dshOpsCompat: '与 dsh-ops 共存',
|
|
@@ -164,48 +169,61 @@ window.__ModuleLoader__.load({
|
|
|
164
169
|
// while its section is mounted, so both sections carry it: a page can render the status
|
|
165
170
|
// section without ever rendering the configure section.
|
|
166
171
|
const CSS = `
|
|
167
|
-
.dw7-
|
|
168
|
-
.dw7-
|
|
169
|
-
.dw7-
|
|
170
|
-
.dw7-
|
|
171
|
-
.dw7-
|
|
172
|
-
.dw7-
|
|
173
|
-
.dw7-
|
|
174
|
-
.dw7-
|
|
175
|
-
.dw7-
|
|
176
|
-
.dw7-
|
|
177
|
-
.dw7-
|
|
178
|
-
.dw7-
|
|
179
|
-
.dw7-
|
|
180
|
-
.dw7-
|
|
181
|
-
.dw7-
|
|
172
|
+
.dw7-settings{display:flex;flex-direction:column;gap:12px;min-width:0;color:var(--dsw-alias-label-primary);font:inherit;}
|
|
173
|
+
.dw7-settings p{margin:0;line-height:1.55;overflow-wrap:anywhere;}
|
|
174
|
+
.dw7-group{display:flex;flex-direction:column;min-width:0;}
|
|
175
|
+
.dw7-card{border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-lg);padding:14px;gap:10px;}
|
|
176
|
+
.dw7-group-head{display:flex;align-items:flex-start;gap:16px;min-width:0;}
|
|
177
|
+
.dw7-card .dw7-group-head{align-items:flex-start;}
|
|
178
|
+
.dw7-group-head .dw7-field-text{display:flex;flex-direction:column;flex:1 1 auto;min-width:0;gap:2px;}
|
|
179
|
+
.dw7-group-head > button[role=switch]{flex:none;margin-top:1px;}
|
|
180
|
+
.dw7-group-title{margin:0 0 4px;font-size:13px;font-weight:600;line-height:20px;color:var(--dsw-alias-label-secondary);}
|
|
181
|
+
.dw7-card .dw7-group-title{margin:0;}
|
|
182
|
+
.dw7-card-body{display:flex;flex-direction:column;min-width:0;padding:12px;border-radius:var(--dsw-radius-md);background:var(--dsw-alias-interactive-bg-hover);}
|
|
183
|
+
.dw7-card-body > .dw7-field{padding:10px 0;}
|
|
184
|
+
.dw7-card-body > .dw7-field:first-child{padding-top:0;}
|
|
185
|
+
.dw7-card-body > .dw7-field:last-child{padding-bottom:0;}
|
|
186
|
+
.dw7-card-body .dw7-field + .dw7-field{border-top:.5px solid var(--dsw-alias-border-l2);}
|
|
182
187
|
.dw7-field{display:flex;flex-direction:column;gap:6px;padding:12px 0;min-width:0;}
|
|
183
188
|
.dw7-field + .dw7-field{border-top:.5px solid var(--dsw-alias-border-l2);}
|
|
184
|
-
.dw7-
|
|
185
|
-
.dw7-
|
|
189
|
+
.dw7-field-text{display:flex;flex-direction:column;flex:1 1 auto;gap:4px;min-width:0;}
|
|
190
|
+
.dw7-field-label{font-size:13px;font-weight:500;line-height:1.5;color:var(--dsw-alias-label-primary);}
|
|
191
|
+
.dw7-field-hint{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary);}
|
|
192
|
+
.dw7-input{box-sizing:border-box;width:100%;height:34px;padding:0 12px;border:.5px solid var(--dsw-alias-border-l4);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;line-height:1.5;}
|
|
186
193
|
.dw7-input:focus-visible{outline:none;border-color:var(--dsw-alias-state-business-primary);}
|
|
187
194
|
.dw7-input:disabled{color:var(--dsw-alias-label-tertiary);cursor:default;}
|
|
188
|
-
.dw7-
|
|
189
|
-
.dw7-
|
|
190
|
-
.dw7-switch-text{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:4px;}
|
|
195
|
+
.dw7-input[aria-invalid=true]{border-color:var(--dsw-alias-state-error-primary);}
|
|
196
|
+
.dw7-field-error{margin:0;font-size:12px;line-height:18px;color:var(--dsw-alias-state-error-primary);}
|
|
191
197
|
.dw7-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding-top:4px;}
|
|
192
198
|
.dw7-save{appearance:none;border:1px solid transparent;border-radius:var(--dsw-radius-md);padding:5px 14px;font:inherit;font-size:13px;line-height:1.5;cursor:pointer;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);}
|
|
193
199
|
.dw7-save:disabled{opacity:.4;cursor:default;}
|
|
194
|
-
.dw7-save:focus-visible,.dw7-reset:focus-visible,.dw7-small:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:1px;}
|
|
195
200
|
.dw7-reset{border:none;background:none;padding:0;font:inherit;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-secondary);cursor:pointer;}
|
|
196
|
-
.dw7-reset:
|
|
197
|
-
.dw7-reset:disabled{cursor:default;}
|
|
201
|
+
.dw7-reset:disabled{opacity:.4;cursor:default;}
|
|
198
202
|
.dw7-small{box-sizing:border-box;height:28px;padding:0 10px;border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-sm);background:transparent;font:inherit;font-size:12px;line-height:18px;color:var(--dsw-alias-label-primary);cursor:pointer;}
|
|
199
|
-
.dw7-small:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);}
|
|
200
203
|
.dw7-small:disabled{opacity:.4;cursor:not-allowed;}
|
|
204
|
+
.dw7-detail{display:flex;flex-direction:column;gap:12px;min-width:0;}
|
|
205
|
+
.dw7-detail-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap;min-width:0;}
|
|
206
|
+
.dw7-detail-title{margin:0;flex:none;font-size:13px;font-weight:600;line-height:20px;color:var(--dsw-alias-label-primary);}
|
|
207
|
+
.dw7-detail-metric{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary);}
|
|
208
|
+
.dw7-detail-action{margin-left:auto;flex:none;}
|
|
209
|
+
.dw7-status-card{display:flex;flex-direction:column;gap:10px;padding:14px;border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-lg);min-width:0;}
|
|
210
|
+
.dw7-status-body{padding:12px;border-radius:var(--dsw-radius-md);background:var(--dsw-alias-interactive-bg-hover);min-width:0;}
|
|
211
|
+
.dw7-status-lead{margin:0 0 12px;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:1.6;}
|
|
212
|
+
.dw7-facts{display:grid;gap:6px;margin:0;}
|
|
213
|
+
.dw7-facts > div{display:grid;grid-template-columns:max-content minmax(0,1fr);align-items:baseline;column-gap:14px;}
|
|
214
|
+
.dw7-facts dt{color:var(--dsw-alias-label-tertiary);font-size:13px;}
|
|
215
|
+
.dw7-facts dd{margin:0;overflow-wrap:anywhere;font-size:13px;}
|
|
216
|
+
.dw7-status-note{margin:12px 0 0;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.6;}
|
|
217
|
+
.dw7-note{margin:0;font-size:12px;line-height:1.6;color:var(--dsw-alias-label-tertiary);}
|
|
201
218
|
.dw7-report{margin:0;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.6;white-space:pre-wrap;overflow-wrap:anywhere;}
|
|
202
|
-
.dw7-empty{margin:0;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:20px;}
|
|
203
219
|
.dw7-error{margin:0;color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px;overflow-wrap:anywhere;}
|
|
204
220
|
.dw7-ok{margin:0;color:var(--dsw-alias-state-success-primary);font-size:12px;line-height:1.5;}
|
|
205
|
-
.dw7-
|
|
206
|
-
.dw7-
|
|
207
|
-
.dw7-
|
|
221
|
+
.dw7-preview{display:flex;flex-direction:column;gap:10px;padding:12px;border-radius:var(--dsw-radius-md);background:var(--dsw-alias-interactive-bg-hover);font-size:13px;}
|
|
222
|
+
.dw7-preview-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap;min-width:0;}
|
|
223
|
+
.dw7-preview-note{margin:0;flex:1;min-width:0;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5;}
|
|
208
224
|
.dw7-code{box-sizing:border-box;margin:0;padding:10px 12px;border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-mono,ui-monospace,SFMono-Regular,Menlo,monospace);font-size:12px;line-height:1.6;white-space:pre-wrap;overflow-wrap:anywhere;}
|
|
225
|
+
.dw7-save:focus-visible,.dw7-reset:focus-visible,.dw7-small:focus-visible,.dw7-input:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:1px;}
|
|
226
|
+
@media(max-width:440px){.dw7-facts > div{grid-template-columns:1fr;gap:2px;}}
|
|
209
227
|
`
|
|
210
228
|
|
|
211
229
|
return {
|
|
@@ -369,40 +387,43 @@ window.__ModuleLoader__.load({
|
|
|
369
387
|
}
|
|
370
388
|
}
|
|
371
389
|
const patchBlock = patch !== null
|
|
372
|
-
? h('div', { className: 'dw7-
|
|
373
|
-
h('div', { className: 'dw7-
|
|
374
|
-
h('p', { className: 'dw7-
|
|
375
|
-
h('button', { className: 'dw7-small', onClick: () => void copyPatch() }, copied ? t('copied') : t('copy_patch')),
|
|
390
|
+
? h('div', { className: 'dw7-preview' },
|
|
391
|
+
h('div', { className: 'dw7-preview-head' },
|
|
392
|
+
h('p', { className: 'dw7-preview-note' }, t('patch_note')),
|
|
393
|
+
h('button', { type: 'button', className: 'dw7-small', onClick: () => void copyPatch() }, copied ? t('copied') : t('copy_patch')),
|
|
376
394
|
copyNote ? h('span', { className: 'dw7-error' }, copyNote) : null),
|
|
377
395
|
h('pre', { className: 'dw7-code' }, patch))
|
|
378
396
|
: null
|
|
379
|
-
|
|
397
|
+
const body = fallback
|
|
398
|
+
? h('p', { className: 'dw7-report', key: 'report' }, report)
|
|
399
|
+
: h('div', { className: 'dw7-status-body', key: 'body' },
|
|
400
|
+
h('p', { className: 'dw7-status-lead' }, view.lead),
|
|
401
|
+
h('dl', { className: 'dw7-facts' },
|
|
402
|
+
h('div', { key: 'status' },
|
|
403
|
+
h('dt', null, t('status_row_status')),
|
|
404
|
+
h('dd', null, view.status)),
|
|
405
|
+
h('div', { key: 'runtime' },
|
|
406
|
+
h('dt', null, t('status_row_runtime')),
|
|
407
|
+
h('dd', null, view.runtime)),
|
|
408
|
+
h('div', { key: 'path' },
|
|
409
|
+
h('dt', null, t('status_row_path')),
|
|
410
|
+
h('dd', null, path ?? t('status_no_path')))),
|
|
411
|
+
h('p', { className: 'dw7-status-note' }, view.note))
|
|
412
|
+
return h('section', { className: 'dw7-detail' },
|
|
380
413
|
h('style', null, CSS),
|
|
381
|
-
h('div', { className: 'dw7-status-
|
|
382
|
-
h('
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
h('div', { key: 'status' },
|
|
395
|
-
h('dt', null, t('status_row_status')),
|
|
396
|
-
h('dd', null, view.status)),
|
|
397
|
-
h('div', { key: 'runtime' },
|
|
398
|
-
h('dt', null, t('status_row_runtime')),
|
|
399
|
-
h('dd', null, view.runtime)),
|
|
400
|
-
h('div', { key: 'path' },
|
|
401
|
-
h('dt', null, t('status_row_path')),
|
|
402
|
-
h('dd', null, path ?? t('status_no_path')))),
|
|
403
|
-
h('small', { className: 'dw7-note' }, view.note)),
|
|
404
|
-
patchBlock,
|
|
405
|
-
snap.error !== null ? h('p', { className: 'dw7-error' }, t('status_error', { error: snap.error })) : null,
|
|
414
|
+
h('div', { className: 'dw7-status-card' },
|
|
415
|
+
h('div', { className: 'dw7-detail-head' },
|
|
416
|
+
h('h3', { className: 'dw7-detail-title' }, t('diagnostics_title')),
|
|
417
|
+
snap.at ? h('span', { className: 'dw7-detail-metric' }, t('refreshed_at', { at: snap.at })) : null,
|
|
418
|
+
h('button', {
|
|
419
|
+
type: 'button',
|
|
420
|
+
className: 'dw7-small dw7-detail-action',
|
|
421
|
+
onClick: () => void refresh(),
|
|
422
|
+
disabled: snap.state === 'loading',
|
|
423
|
+
}, snap.state === 'loading' ? t('redetecting') : t('redetect'))),
|
|
424
|
+
body,
|
|
425
|
+
patchBlock,
|
|
426
|
+
snap.error !== null ? h('p', { className: 'dw7-error' }, t('status_error', { error: snap.error })) : null),
|
|
406
427
|
)
|
|
407
428
|
}
|
|
408
429
|
|
|
@@ -455,35 +476,45 @@ window.__ModuleLoader__.load({
|
|
|
455
476
|
}
|
|
456
477
|
}
|
|
457
478
|
|
|
458
|
-
return h('
|
|
479
|
+
return h('form', {
|
|
480
|
+
className: 'dw7-settings',
|
|
481
|
+
onSubmit: (event) => {
|
|
482
|
+
event.preventDefault()
|
|
483
|
+
void save()
|
|
484
|
+
},
|
|
485
|
+
},
|
|
459
486
|
h('style', null, CSS),
|
|
460
|
-
h('div', { className: 'dw7-head' },
|
|
461
|
-
h('h4', { className: 'dw7-title' }, t('card_title'))),
|
|
462
487
|
loading || unavailable
|
|
463
|
-
? h('p', { className: 'dw7-
|
|
488
|
+
? h('p', { className: 'dw7-note' }, loading ? t('form_loading') : t('form_unavailable'))
|
|
464
489
|
: [
|
|
465
|
-
h('
|
|
466
|
-
h('
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
490
|
+
h('section', { className: 'dw7-group dw7-card', key: 'executor' },
|
|
491
|
+
h('div', { className: 'dw7-group-head' },
|
|
492
|
+
h('div', { className: 'dw7-field-text' },
|
|
493
|
+
h('h4', { className: 'dw7-group-title' }, t('executor_title')),
|
|
494
|
+
h('p', { className: 'dw7-field-hint' }, t('executor_hint')))),
|
|
495
|
+
h('div', { className: 'dw7-card-body' },
|
|
496
|
+
h('div', { className: 'dw7-field' },
|
|
497
|
+
h('label', { className: 'dw7-field-label', htmlFor: 'dw7-override-path' }, t('field_overridePath')),
|
|
498
|
+
h('input', {
|
|
499
|
+
id: 'dw7-override-path',
|
|
500
|
+
className: 'dw7-input',
|
|
501
|
+
type: 'text',
|
|
502
|
+
spellCheck: false,
|
|
503
|
+
value: values.overridePath,
|
|
504
|
+
disabled: !editable,
|
|
505
|
+
placeholder: 'C:\\Program Files\\PowerShell\\7\\pwsh.exe',
|
|
506
|
+
onChange: (event) => {
|
|
507
|
+
setDraft({ ...values, overridePath: event.target.value })
|
|
508
|
+
setNote(null)
|
|
509
|
+
setSavedAt(null)
|
|
510
|
+
},
|
|
511
|
+
}),
|
|
512
|
+
h('p', { className: 'dw7-field-hint' }, t('overridePath_hint'))))),
|
|
513
|
+
h('section', { className: 'dw7-group dw7-card', key: 'compat' },
|
|
514
|
+
h('div', { className: 'dw7-group-head' },
|
|
515
|
+
h('div', { className: 'dw7-field-text' },
|
|
516
|
+
h('h4', { className: 'dw7-group-title' }, t('field_dshOpsCompat')),
|
|
517
|
+
h('p', { className: 'dw7-field-hint' }, t('dshOpsCompat_hint'))),
|
|
487
518
|
h(Switch, {
|
|
488
519
|
checked: values.dshOpsCompat,
|
|
489
520
|
disabled: !editable,
|
|
@@ -496,11 +527,12 @@ window.__ModuleLoader__.load({
|
|
|
496
527
|
}))),
|
|
497
528
|
h('div', { className: 'dw7-actions', key: 'actions' },
|
|
498
529
|
h('button', {
|
|
530
|
+
type: 'submit',
|
|
499
531
|
className: 'dw7-save',
|
|
500
|
-
onClick: () => void save(),
|
|
501
532
|
disabled: !editable || (!dirty && note === null),
|
|
502
533
|
}, t('save')),
|
|
503
534
|
h('button', {
|
|
535
|
+
type: 'button',
|
|
504
536
|
className: 'dw7-reset',
|
|
505
537
|
onClick: () => {
|
|
506
538
|
setDraft(null)
|
package/index.js
CHANGED
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
mountDshOpsBridge,
|
|
52
52
|
removeDshOpsCompatPatch,
|
|
53
53
|
} from './lib/dsh-ops-bridge.js'
|
|
54
|
+
import { mountUtf8ConsoleBridge } from './lib/utf8-console-bridge.js'
|
|
54
55
|
|
|
55
56
|
export const name = 'dsh-win-pwsh7'
|
|
56
57
|
export const inject = ['subprocess', 'tools', 'configEditor', 'systemPrompt']
|
|
@@ -70,6 +71,9 @@ export const Config = z.object({
|
|
|
70
71
|
// Default-on after the multi-session validation of the pre-restriction bridge. Turning it
|
|
71
72
|
// off removes the managed compatibility override; turning it on restores it.
|
|
72
73
|
dshOpsCompat: z.boolean().default(true).volatile(),
|
|
74
|
+
// Transitional Windows console code page fix, owned by lib/utf8-console-bridge.js.
|
|
75
|
+
// Delete this field together with the bridge and its mount (see README removal contract).
|
|
76
|
+
utf8Console: z.boolean().default(true).volatile(),
|
|
73
77
|
})
|
|
74
78
|
|
|
75
79
|
/**
|
|
@@ -374,6 +378,10 @@ export function apply(ctx, config) {
|
|
|
374
378
|
|
|
375
379
|
ctx.effect(() => mountDshOpsBridge(ctx, current), 'dsh-win-pwsh7: hide duplicate official tools while dsh-ops is compatible')
|
|
376
380
|
|
|
381
|
+
// Transitional, removable: delete this mount and lib/utf8-console-bridge.js once the Host
|
|
382
|
+
// pins the console code page itself. The README records the removal contract.
|
|
383
|
+
ctx.effect(() => mountUtf8ConsoleBridge(ctx, current), 'dsh-win-pwsh7: pin the Windows console code page for pwsh output')
|
|
384
|
+
|
|
377
385
|
ctx.effect(() => {
|
|
378
386
|
void activationSweep(ctx, current()).then((report) => {
|
|
379
387
|
ctx.logger?.info?.('dsh-win-pwsh7: %s', flattenReport(report))
|
|
@@ -533,12 +541,14 @@ function readRowConfig(ctx, fallback) {
|
|
|
533
541
|
const merged = {
|
|
534
542
|
overridePath: typeof fallback?.overridePath === 'string' ? fallback.overridePath : undefined,
|
|
535
543
|
dshOpsCompat: fallback?.dshOpsCompat !== false,
|
|
544
|
+
utf8Console: fallback?.utf8Console !== false,
|
|
536
545
|
}
|
|
537
546
|
try {
|
|
538
547
|
const entry = ctx.configEditor.entries().find((candidate) => candidate?.options?.name === 'dsh-win-pwsh7')
|
|
539
548
|
const values = entry?.options?.config
|
|
540
549
|
if (isSet(values?.overridePath)) merged.overridePath = String(values.overridePath)
|
|
541
550
|
if (values?.dshOpsCompat !== undefined) merged.dshOpsCompat = values.dshOpsCompat !== false
|
|
551
|
+
if (values?.utf8Console !== undefined) merged.utf8Console = values.utf8Console !== false
|
|
542
552
|
} catch { /* fall back to the captured config */ }
|
|
543
553
|
return merged
|
|
544
554
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transitional Windows UTF-8 console bridge.
|
|
3
|
+
*
|
|
4
|
+
* The Host's `dsh-pwsh-local` collector decodes stdout as UTF-8, but a nested
|
|
5
|
+
* `pwsh -File` / `powershell -File` started by a tool command inherits the console
|
|
6
|
+
* code page (936 on a Simplified Chinese machine) and emits GBK. The Host preamble
|
|
7
|
+
* only pins the .NET output encoding of the outer process; it does not change the
|
|
8
|
+
* console code page. This module wraps the same `ctx.shell` executor the `pwsh` tool
|
|
9
|
+
* uses and prepends `chcp.com 65001` so the code page is right before the command
|
|
10
|
+
* runs.
|
|
11
|
+
*
|
|
12
|
+
* It is deliberately a leaf: no imports from the rest of the plugin, one mount in
|
|
13
|
+
* `index.js`, one config field, and a documented removal contract. Delete this file,
|
|
14
|
+
* the import/mount and the config field when the Host ships the same fix.
|
|
15
|
+
*
|
|
16
|
+
* REMOVE WHEN: the Host's `@deepseek-ai/dsh-pwsh-local` pins the console code page
|
|
17
|
+
* itself (its `ENCODING_PREAMBLE` carries the `chcp.com 65001` switch), or a nested
|
|
18
|
+
* `pwsh -File` under code page 936 already emits UTF-8 without this plugin. Follow
|
|
19
|
+
* the README removal contract in the same change.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** One line of the wrapper's own diagnostics. */
|
|
23
|
+
const describe = (error) => (error instanceof Error ? error.message : String(error))
|
|
24
|
+
|
|
25
|
+
/** Marks an executor this module already wrapped, so HMR or a second mount cannot stack. */
|
|
26
|
+
export const UTF8_CONSOLE_PATCH = Symbol.for('dsh-win-pwsh7.utf8ConsoleBridge')
|
|
27
|
+
|
|
28
|
+
/** A command that already selects UTF-8 must not be prefixed twice. */
|
|
29
|
+
const ALREADY_UTF8 = /^\s*chcp(?:\.com)?\s+65001(?:\s|>|$)/iu
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Prefix a Windows command with the console code page switch.
|
|
33
|
+
*
|
|
34
|
+
* @param {unknown} command - the caller's command text.
|
|
35
|
+
* @returns the command, prefixed once when it is a string and not already pinned.
|
|
36
|
+
*/
|
|
37
|
+
export function utf8Command(command) {
|
|
38
|
+
if (typeof command !== 'string' || command.length === 0) return command
|
|
39
|
+
if (ALREADY_UTF8.test(command)) return command
|
|
40
|
+
return `chcp.com 65001 > $null; ${command}`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Wrap the live shell executor the `pwsh` tool resolves.
|
|
45
|
+
*
|
|
46
|
+
* Fails open: a missing service, a non-writable executor or a shape change leaves
|
|
47
|
+
* the plugin working exactly as before and logs one warning. The wrapper is removed
|
|
48
|
+
* with the plugin's fiber; it never writes files, config or profile state.
|
|
49
|
+
*
|
|
50
|
+
* @param ctx - the row's Host context.
|
|
51
|
+
* @param current - reads the live row configuration.
|
|
52
|
+
* @param platform - injectable for tests; defaults to `process.platform`.
|
|
53
|
+
* @returns void; the wrap is owned by the injected fiber.
|
|
54
|
+
*/
|
|
55
|
+
export function mountUtf8ConsoleBridge(ctx, current, platform = process.platform) {
|
|
56
|
+
if (platform !== 'win32') return
|
|
57
|
+
const warn = (message) => ctx.logger?.warn?.(`dsh-win-pwsh7: ${message}`)
|
|
58
|
+
if (typeof ctx.inject !== 'function') return
|
|
59
|
+
try {
|
|
60
|
+
ctx.inject(['shell'], (scoped) => {
|
|
61
|
+
try {
|
|
62
|
+
const viaGet = typeof scoped.get === 'function' ? scoped.get('shell') : undefined
|
|
63
|
+
const shell = viaGet ?? scoped.shell
|
|
64
|
+
if (shell === undefined || shell === null) return
|
|
65
|
+
if (shell[UTF8_CONSOLE_PATCH] === true) return
|
|
66
|
+
const original = shell.execute
|
|
67
|
+
if (typeof original !== 'function') {
|
|
68
|
+
warn('the shell service exposes no execute(); the UTF-8 console bridge stays off')
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
shell.execute = function executeWithUtf8Console(spec) {
|
|
72
|
+
try {
|
|
73
|
+
if (current?.()?.utf8Console === false) return original.call(this, spec)
|
|
74
|
+
if (spec === null || typeof spec !== 'object' || typeof spec.command !== 'string') {
|
|
75
|
+
return original.call(this, spec)
|
|
76
|
+
}
|
|
77
|
+
const command = utf8Command(spec.command)
|
|
78
|
+
if (command === spec.command) return original.call(this, spec)
|
|
79
|
+
return original.call(this, { ...spec, command })
|
|
80
|
+
} catch (error) {
|
|
81
|
+
warn(`the UTF-8 console bridge fell open: ${describe(error)}`)
|
|
82
|
+
return original.call(this, spec)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
Object.defineProperty(shell, UTF8_CONSOLE_PATCH, { value: true, configurable: true })
|
|
86
|
+
const restore = () => {
|
|
87
|
+
try {
|
|
88
|
+
shell.execute = original
|
|
89
|
+
} catch { /* the executor was replaced by another owner */ }
|
|
90
|
+
try {
|
|
91
|
+
delete shell[UTF8_CONSOLE_PATCH]
|
|
92
|
+
} catch { /* non-configurable; the marker is advisory */ }
|
|
93
|
+
}
|
|
94
|
+
if (typeof scoped.effect === 'function') {
|
|
95
|
+
scoped.effect(() => restore, 'dsh-win-pwsh7: utf8 console bridge')
|
|
96
|
+
}
|
|
97
|
+
} catch (error) {
|
|
98
|
+
warn(`the UTF-8 console bridge could not attach: ${describe(error)}`)
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
} catch (error) {
|
|
102
|
+
warn(`the UTF-8 console bridge could not inject the shell service: ${describe(error)}`)
|
|
103
|
+
}
|
|
104
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-win-pwsh7",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Detect and validate PowerShell 7 for the Harness pwsh tool on Windows, and install it on request through the Microsoft Store — scripted or manual — while the pwsh tool stays blocked until a PowerShell 7 runtime actually resolves.",
|
|
6
6
|
"engines": {
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"client.js",
|
|
34
34
|
"lib/typert.host.js",
|
|
35
35
|
"lib/dsh-ops-bridge.js",
|
|
36
|
+
"lib/utf8-console-bridge.js",
|
|
36
37
|
"cordis.patch.yml",
|
|
37
38
|
"locale/*.json",
|
|
38
39
|
"icon.svg",
|
|
@@ -51,7 +52,7 @@
|
|
|
51
52
|
"windows"
|
|
52
53
|
],
|
|
53
54
|
"scripts": {
|
|
54
|
-
"check": "node tools/check-host.mjs && node tools/check-client.mjs && node tools/check-native-contracts.mjs",
|
|
55
|
+
"check": "node tools/check-host.mjs && node tools/check-client.mjs && node tools/check-utf8-bridge.mjs && node tools/check-native-contracts.mjs",
|
|
55
56
|
"check:contracts": "node tools/check-native-contracts.mjs",
|
|
56
57
|
"prepublishOnly": "npm run check"
|
|
57
58
|
},
|
package/tools/check-client.mjs
CHANGED
|
@@ -176,10 +176,19 @@ check('the card renders every hint in Chinese', () => {
|
|
|
176
176
|
check('the summary view renders nothing, as the row list expects', () => {
|
|
177
177
|
assert.equal(card({ view: 'summary' }), null)
|
|
178
178
|
})
|
|
179
|
-
check('the configure surface
|
|
179
|
+
check('the configure surface uses the dsh-ops card language', () => {
|
|
180
|
+
// Aligned with the installed dsh-ops branch: one hairline card per group, a shaded
|
|
181
|
+
// card body for editable fields, and the toggle in the card head.
|
|
180
182
|
const sections = elementsOf(cardTree, 'section')
|
|
181
|
-
assert.equal(sections.length,
|
|
182
|
-
|
|
183
|
+
assert.equal(sections.length, 2, `expected two cards, found ${sections.length}`)
|
|
184
|
+
for (const section of sections) {
|
|
185
|
+
assert.match(String(section.props.className), /\bdw7-card\b/u)
|
|
186
|
+
assert.match(String(section.props.className), /\bdw7-group\b/u)
|
|
187
|
+
}
|
|
188
|
+
const bodies = elementsOf(cardTree, 'div').filter(node => node.props.className === 'dw7-card-body')
|
|
189
|
+
assert.equal(bodies.length, 1, `expected one shaded body for the editable card, found ${bodies.length}`)
|
|
190
|
+
assert.ok(!elementsOf(cardTree, 'section').some(node => /\bdw7-section\b/u.test(String(node.props.className))),
|
|
191
|
+
'the retired flat section class survived')
|
|
183
192
|
})
|
|
184
193
|
|
|
185
194
|
// A second application with no controller for this row — the state that used to throw inside the
|
|
@@ -256,16 +265,28 @@ check('the en dictionary carries no Chinese', () => {
|
|
|
256
265
|
const cjk = en.match(/[\u4e00-\u9fff]/gu)
|
|
257
266
|
assert.equal(cjk, null, `Chinese in the en dictionary: ${cjk === null ? '' : cjk.join('')}`)
|
|
258
267
|
})
|
|
259
|
-
check('one
|
|
268
|
+
check('one card-language stylesheet covers both surfaces', () => {
|
|
260
269
|
assert.ok(/const CSS = `/u.test(source), 'the shared stylesheet is missing')
|
|
261
270
|
assert.ok(!/CSS_CARD|CSS_DIAG/u.test(source), 'the old per-card stylesheets survived')
|
|
262
|
-
for (const selector of ['.dw7-
|
|
271
|
+
for (const selector of ['.dw7-card{', '.dw7-card-body{', '.dw7-group-title{', '.dw7-detail{', '.dw7-status-card{',
|
|
272
|
+
'.dw7-status-body{', '.dw7-facts{', '.dw7-input{', '.dw7-save{', '.dw7-small{', '.dw7-code{']) {
|
|
263
273
|
assert.ok(source.includes(selector), `the stylesheet does not define ${selector}`)
|
|
264
274
|
}
|
|
275
|
+
// The dsh-ops branch's card chrome: a 0.5px hairline on radius-lg with a shaded body,
|
|
276
|
+
// 13px/600 group titles, and description-list labels sized to their own text.
|
|
277
|
+
assert.ok(source.includes('border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-lg);padding:14px;gap:10px;'),
|
|
278
|
+
'the card chrome no longer matches the dsh-ops card')
|
|
279
|
+
assert.ok(source.includes('background:var(--dsw-alias-interactive-bg-hover);'),
|
|
280
|
+
'the card body no longer uses the sunken surface')
|
|
281
|
+
assert.ok(source.includes('font-size:13px;font-weight:600;line-height:20px;'),
|
|
282
|
+
'the group title metrics no longer match the dsh-ops card')
|
|
283
|
+
assert.ok(source.includes('grid-template-columns:max-content minmax(0,1fr);align-items:baseline;column-gap:14px;'),
|
|
284
|
+
'the fact list no longer uses the native max-content label column')
|
|
265
285
|
const start = source.indexOf('const CSS = `')
|
|
266
286
|
const css = source.slice(start, source.indexOf('`', start + 20))
|
|
267
287
|
assert.ok(!/#[0-9a-f]{3,8}\b/iu.test(css), 'the stylesheet hard-codes a color instead of using a theme token')
|
|
268
288
|
})
|
|
289
|
+
|
|
269
290
|
check('no metered-network copy or branch remains', () => {
|
|
270
291
|
assert.ok(!/metered/iu.test(source), 'the removed metered-network feature still has copy or a branch')
|
|
271
292
|
})
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate for the transitional Windows UTF-8 console bridge.
|
|
3
|
+
*
|
|
4
|
+
* The bridge wraps the Host's `ctx.shell.execute`; this suite drives it with a fake
|
|
5
|
+
* executor so the prefix, the opt-out, the fail-open paths and the restore are
|
|
6
|
+
* verifiable without a DSH installation.
|
|
7
|
+
*/
|
|
8
|
+
import assert from 'node:assert/strict'
|
|
9
|
+
import { mountUtf8ConsoleBridge, utf8Command } from '../lib/utf8-console-bridge.js'
|
|
10
|
+
|
|
11
|
+
let failures = 0
|
|
12
|
+
function check(label, assertion) {
|
|
13
|
+
try {
|
|
14
|
+
assertion()
|
|
15
|
+
} catch (error) {
|
|
16
|
+
failures += 1
|
|
17
|
+
console.error(`FAIL — ${label}: ${error.message}`)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function fakeContext(shell, options = {}) {
|
|
22
|
+
const effects = []
|
|
23
|
+
const warnings = []
|
|
24
|
+
const injected = []
|
|
25
|
+
const scoped = {
|
|
26
|
+
get: () => shell,
|
|
27
|
+
effect(fn) { effects.push(fn()) },
|
|
28
|
+
}
|
|
29
|
+
const ctx = {
|
|
30
|
+
logger: { warn: (message) => warnings.push(message) },
|
|
31
|
+
inject(deps, callback) {
|
|
32
|
+
injected.push(deps)
|
|
33
|
+
callback(scoped)
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
return { ctx, effects, warnings, injected }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function fakeShell() {
|
|
40
|
+
const calls = []
|
|
41
|
+
const shell = {
|
|
42
|
+
execute(spec) {
|
|
43
|
+
calls.push({ spec, self: this })
|
|
44
|
+
return spec
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
return { shell, calls }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// --- The pure command transform -----------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
check('prefixes a bare command once', () => {
|
|
53
|
+
assert.equal(utf8Command('Get-Location'), 'chcp.com 65001 > $null; Get-Location')
|
|
54
|
+
})
|
|
55
|
+
check('leaves an already-pinned command alone', () => {
|
|
56
|
+
for (const command of [
|
|
57
|
+
'chcp 65001 > $null; Get-Location',
|
|
58
|
+
'chcp.com 65001 > $null; Get-Location',
|
|
59
|
+
' chcp.com 65001 >$null; Get-Location',
|
|
60
|
+
]) {
|
|
61
|
+
assert.equal(utf8Command(command), command)
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
check('leaves non-strings and empty strings alone', () => {
|
|
65
|
+
for (const value of [undefined, null, 42, '']) assert.equal(utf8Command(value), value)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
// --- The executor wrapper ----------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
check('wraps the shell executor and prefixes the command', () => {
|
|
71
|
+
const { shell, calls } = fakeShell()
|
|
72
|
+
const { ctx } = fakeContext(shell)
|
|
73
|
+
mountUtf8ConsoleBridge(ctx, () => ({ utf8Console: true }), 'win32')
|
|
74
|
+
const spec = { command: 'pwsh -NoProfile -File enc_test.ps1', cwd: 'C:\\work', timeoutMs: 1000 }
|
|
75
|
+
shell.execute(spec)
|
|
76
|
+
assert.equal(calls.length, 1)
|
|
77
|
+
assert.equal(calls[0].spec.command, 'chcp.com 65001 > $null; pwsh -NoProfile -File enc_test.ps1')
|
|
78
|
+
assert.equal(calls[0].spec.cwd, 'C:\\work')
|
|
79
|
+
assert.equal(calls[0].spec.timeoutMs, 1000)
|
|
80
|
+
assert.equal(calls[0].self, shell)
|
|
81
|
+
})
|
|
82
|
+
check('passes through when the config field is off', () => {
|
|
83
|
+
const { shell, calls } = fakeShell()
|
|
84
|
+
const { ctx } = fakeContext(shell)
|
|
85
|
+
mountUtf8ConsoleBridge(ctx, () => ({ utf8Console: false }), 'win32')
|
|
86
|
+
shell.execute({ command: 'Get-Location' })
|
|
87
|
+
assert.equal(calls[0].spec.command, 'Get-Location')
|
|
88
|
+
})
|
|
89
|
+
check('does not double-prefix a command that already selects UTF-8', () => {
|
|
90
|
+
const { shell, calls } = fakeShell()
|
|
91
|
+
const { ctx } = fakeContext(shell)
|
|
92
|
+
mountUtf8ConsoleBridge(ctx, () => ({ utf8Console: true }), 'win32')
|
|
93
|
+
shell.execute({ command: 'chcp.com 65001 > $null; Get-Location' })
|
|
94
|
+
assert.equal(calls[0].spec.command, 'chcp.com 65001 > $null; Get-Location')
|
|
95
|
+
})
|
|
96
|
+
check('is idempotent when mounted twice on the same executor', () => {
|
|
97
|
+
const { shell } = fakeShell()
|
|
98
|
+
const { ctx } = fakeContext(shell)
|
|
99
|
+
mountUtf8ConsoleBridge(ctx, () => ({ utf8Console: true }), 'win32')
|
|
100
|
+
const once = shell.execute
|
|
101
|
+
mountUtf8ConsoleBridge(ctx, () => ({ utf8Console: true }), 'win32')
|
|
102
|
+
assert.equal(shell.execute, once)
|
|
103
|
+
})
|
|
104
|
+
check('restores the original executor when the fiber disposes', () => {
|
|
105
|
+
const { shell, calls } = fakeShell()
|
|
106
|
+
const original = shell.execute
|
|
107
|
+
const { ctx, effects } = fakeContext(shell)
|
|
108
|
+
mountUtf8ConsoleBridge(ctx, () => ({ utf8Console: true }), 'win32')
|
|
109
|
+
assert.notEqual(shell.execute, original)
|
|
110
|
+
effects.forEach((dispose) => dispose())
|
|
111
|
+
assert.equal(shell.execute, original)
|
|
112
|
+
shell.execute({ command: 'Get-Location' })
|
|
113
|
+
assert.equal(calls.at(-1).spec.command, 'Get-Location')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
// --- Fail-open paths ---------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
check('does nothing on non-Windows', () => {
|
|
119
|
+
const { shell } = fakeShell()
|
|
120
|
+
const { ctx, injected } = fakeContext(shell)
|
|
121
|
+
mountUtf8ConsoleBridge(ctx, () => ({ utf8Console: true }), 'linux')
|
|
122
|
+
assert.deepEqual(injected, [])
|
|
123
|
+
})
|
|
124
|
+
check('warns and leaves a service without execute untouched', () => {
|
|
125
|
+
const { ctx, warnings } = fakeContext({})
|
|
126
|
+
mountUtf8ConsoleBridge(ctx, () => ({ utf8Console: true }), 'win32')
|
|
127
|
+
assert.equal(warnings.length, 1)
|
|
128
|
+
assert.match(warnings[0], /no execute/u)
|
|
129
|
+
})
|
|
130
|
+
check('warns instead of throwing on a frozen executor', () => {
|
|
131
|
+
const shell = Object.freeze({ execute(spec) { return spec } })
|
|
132
|
+
const { ctx, warnings } = fakeContext(shell)
|
|
133
|
+
assert.doesNotThrow(() => mountUtf8ConsoleBridge(ctx, () => ({ utf8Console: true }), 'win32'))
|
|
134
|
+
assert.equal(warnings.length, 1)
|
|
135
|
+
})
|
|
136
|
+
check('falls open when the config reader throws', () => {
|
|
137
|
+
const { shell, calls } = fakeShell()
|
|
138
|
+
const { ctx } = fakeContext(shell)
|
|
139
|
+
mountUtf8ConsoleBridge(ctx, () => { throw new Error('config unavailable') }, 'win32')
|
|
140
|
+
shell.execute({ command: 'Get-Location' })
|
|
141
|
+
assert.equal(calls[0].spec.command, 'Get-Location')
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
if (failures > 0) {
|
|
145
|
+
console.error(`utf8 bridge: ${failures} check(s) failed`)
|
|
146
|
+
process.exit(1)
|
|
147
|
+
}
|
|
148
|
+
console.log('ok — utf8 console bridge: prefix, opt-out, idempotence, restore and fail-open paths')
|