zen-gitsync 2.16.28 → 2.16.30

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.
Files changed (80) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/src/cli/ai/platformGuard.js +200 -200
  4. package/src/cli/ai/platformGuard.test.js +253 -253
  5. package/src/gitCommit.js +484 -484
  6. package/src/ui/public/assets/AgentView-Cp9Xgs_B.js +4 -0
  7. package/src/ui/public/assets/AppVersionBadge-DcgY2eSV.js +2 -0
  8. package/src/ui/public/assets/BranchSelector-Bc_nwnaf.js +1 -0
  9. package/src/ui/public/assets/CommitForm-DmTaYwYF.js +6 -0
  10. package/src/ui/public/assets/CommonDialog-Brk9ZIx_.js +1 -0
  11. package/src/ui/public/assets/EditorView-BXcPstel.js +1 -0
  12. package/src/ui/public/assets/{FlowExecutionViewer-B7CdeiSp.js → FlowExecutionViewer-DT4WJ3H6.js} +1 -1
  13. package/src/ui/public/assets/FlowOrchestrationWorkspace-kzpWFreh.js +21 -0
  14. package/src/ui/public/assets/LogList-BlJplIi1.css +1 -0
  15. package/src/ui/public/assets/LogList-FyPUAVK6.js +7 -0
  16. package/src/ui/public/assets/MindmapView-qul1kc2j.js +2 -0
  17. package/src/ui/public/assets/MonitorView-CJO-jTlG.js +1 -0
  18. package/src/ui/public/assets/ProjectStartupButton-oZJIaL1F.js +1 -0
  19. package/src/ui/public/assets/{RemoteRepoCard-CGn4tRy-.js → RemoteRepoCard-Cyz5QDia.js} +2 -2
  20. package/src/ui/public/assets/SourceMapView-BXHG1JE_.js +3 -0
  21. package/src/ui/public/assets/{SvgIcon-CL2qPswh.js → SvgIcon-DjCKMwo1.js} +1 -1
  22. package/src/ui/public/assets/UserInputNode-lSHdQ7nQ.js +1 -0
  23. package/src/ui/public/assets/WorkbenchView-BSupsbko.js +28 -0
  24. package/src/ui/public/assets/{_plugin-vue_export-helper-D3_upOKo.js → _plugin-vue_export-helper-DExUTKTW.js} +5 -3
  25. package/src/ui/public/assets/configStore-BFc69UjA.js +1 -0
  26. package/src/ui/public/assets/{css.worker-CvXBzhp8.js → css.worker-Wv5dxAWO.js} +1 -1
  27. package/src/ui/public/assets/{dagre-CWIGcX9d.js → dagre-CyRyf1aT.js} +1 -1
  28. package/src/ui/public/assets/element-plus--ayTjGvF.js +25 -0
  29. package/src/ui/public/assets/element-plus-D3Dn7nX0.css +1 -0
  30. package/src/ui/public/assets/{flow-mindmap-B5aqOviR.js → flow-mindmap-BcBVGR99.js} +38 -38
  31. package/src/ui/public/assets/{html.worker-BO6WuOEO.js → html.worker-CQP8QQsS.js} +1 -1
  32. package/src/ui/public/assets/index-DX3FHIo4.js +79 -0
  33. package/src/ui/public/assets/index-zmReBTKj.css +1 -0
  34. package/src/ui/public/assets/{json.worker-BkJRGcCJ.js → json.worker-DzV-CpCQ.js} +1 -1
  35. package/src/ui/public/assets/{monaco-BFLoT2VD.js → monaco-43aputPh.js} +198 -198
  36. package/src/ui/public/assets/socket-io-BZIzo1US.js +1 -0
  37. package/src/ui/public/assets/toolsStore-B2zt5KJ0.js +1 -0
  38. package/src/ui/public/assets/{ts.worker-B0J26iPs.js → ts.worker-Dth06zuC.js} +15 -15
  39. package/src/ui/public/assets/{vendor-Cx8PA1NP.js → vendor-Dan_vdyX.js} +14 -14
  40. package/src/ui/public/assets/{vue-flow-COROsN-V.js → vue-flow-CUR1dbQb.js} +2 -2
  41. package/src/ui/public/favicon.svg +75 -75
  42. package/src/ui/public/index.html +32 -32
  43. package/src/ui/public/logo.svg +74 -74
  44. package/src/ui/server/middleware/requestLogger.js +53 -53
  45. package/src/ui/server/routes/branchStatus.js +105 -105
  46. package/src/ui/server/routes/git/diff.js +426 -426
  47. package/src/ui/server/routes/git/diffUtils.js +149 -149
  48. package/src/ui/server/routes/git/stash.js +550 -550
  49. package/src/ui/server/routes/git/tags.js +176 -176
  50. package/src/ui/server/routes/process.js +86 -86
  51. package/src/ui/server/routes/status.js +67 -67
  52. package/src/ui/server/routes/workbench/pdfText.js +145 -145
  53. package/src/ui/server/utils/createSavePortToFile.js +48 -48
  54. package/src/ui/server/utils/instanceRegistry.js +282 -282
  55. package/src/ui/server/utils/startServerOnAvailablePort.js +116 -116
  56. package/src/ui/public/assets/AgentView-Bytr0HbV.js +0 -4
  57. package/src/ui/public/assets/AppVersionBadge-DTsgbuSQ.js +0 -2
  58. package/src/ui/public/assets/BranchSelector-C9OPOmaJ.js +0 -1
  59. package/src/ui/public/assets/CommitForm-Cr5Ajewf.js +0 -6
  60. package/src/ui/public/assets/CommonDialog-CQz6iEqp.js +0 -1
  61. package/src/ui/public/assets/EditorView-BYoWEp7_.js +0 -1
  62. package/src/ui/public/assets/FlowOrchestrationWorkspace--Sguba2_.js +0 -21
  63. package/src/ui/public/assets/LogList-Bo53PBwg.css +0 -1
  64. package/src/ui/public/assets/LogList-CF47CGZw.js +0 -7
  65. package/src/ui/public/assets/MindmapView-DR5bT_oQ.js +0 -2
  66. package/src/ui/public/assets/MonitorView-DoLNabxl.js +0 -1
  67. package/src/ui/public/assets/ProjectStartupButton-BiNiH43W.js +0 -1
  68. package/src/ui/public/assets/SourceMapView-DhxMPULM.js +0 -3
  69. package/src/ui/public/assets/UserInputNode-BftMuqji.js +0 -1
  70. package/src/ui/public/assets/WorkbenchView-CJwU_MsO.js +0 -28
  71. package/src/ui/public/assets/configStore-Cqnbpz5-.js +0 -1
  72. package/src/ui/public/assets/element-plus-B3pTS_5R.js +0 -32
  73. package/src/ui/public/assets/element-plus-CQrjg9us.css +0 -1
  74. package/src/ui/public/assets/index-BgKFctmf.js +0 -71
  75. package/src/ui/public/assets/index-DnD8_s3s.css +0 -1
  76. package/src/ui/public/assets/socket-io-Bcae2P7B.js +0 -1
  77. package/src/ui/public/assets/toolsStore-D9gXnqwO.js +0 -1
  78. package/src/ui/server/.claude/codediff.txt +0 -8
  79. /package/src/ui/public/assets/{editor.worker-Cn2oRESe.js → editor.worker-Bd9IXS8d.js} +0 -0
  80. /package/src/ui/public/assets/{rolldown-runtime-CMxvf4Kt.js → rolldown-runtime-BM3Ffeng.js} +0 -0
package/README.md CHANGED
@@ -142,7 +142,7 @@ The GUI runs as a local web server and opens in your default browser. It attache
142
142
  | Reset to remote | One-click `git reset --hard origin/<branch>`; auto-refreshes branch info first to avoid stale-branch targets; hidden when working tree is clean and no unpushed commits |
143
143
  | Merge | Merge another branch; detects and surfaces in-progress merge state |
144
144
  | Diff viewer | Monaco-based side-by-side diff for any changed file |
145
- | In-diff preview | Toggle a preview pane below the diff for `.html` / `.htm` / `.svg` (sandboxed iframe) and `.md` / `.markdown` (rendered Markdown) — same preview experience as the built-in editor, with a draggable vertical resizer; split ratio is persisted per project |
145
+ | In-diff preview | Toggle a preview pane below the diff for `.html` / `.htm` / `.svg` (sandboxed iframe with JavaScript enabled — interactive reports work, isolated from the app via an opaque origin) and `.md` / `.markdown` (rendered Markdown) — same preview experience as the built-in editor, with a draggable vertical resizer; split ratio is persisted per project |
146
146
  | Commit log | Browse commit history with author, date, branch tags, and changed files |
147
147
  | Remote URL | Display and one-click copy the remote repository URL |
148
148
  | Auto-refresh | Silently refreshes status and branch info when the window gains focus, the tab becomes visible, or you switch back to the **Git** view in the Activity Bar |
@@ -680,7 +680,7 @@ GUI 以本地 Web 服务器形式运行,自动在浏览器中打开,并附
680
680
  | 重置到远程 | 一键执行 `git reset --hard origin/<branch>`;点击前会先刷新分支信息,避免重置到陈旧分支;当工作区干净且无未推送提交时按钮自动隐藏 |
681
681
  | 合并 | 合并其他分支,自动检测并引导处理合并中间状态 |
682
682
  | Diff 查看器 | 基于 Monaco 编辑器的并排文件差异视图 |
683
- | 差异内预览 | 在差异下方一键展开预览面板:`.html` / `.htm` / `.svg` 走沙箱化 iframe,`.md` / `.markdown` 走 Markdown 渲染,与内置编辑器一致的预览体验;上下比例可拖拽,按项目持久化 |
683
+ | 差异内预览 | 在差异下方一键展开预览面板:`.html` / `.htm` / `.svg` 走沙箱化 iframe(允许 JS 执行,报告类页面的按钮/交互可用,同时以不透明 origin 与宿主应用隔离),`.md` / `.markdown` 走 Markdown 渲染,与内置编辑器一致的预览体验;上下比例可拖拽,按项目持久化 |
684
684
  | 提交日志 | 浏览历史提交(作者、时间、分支标签、变更文件) |
685
685
  | 远程地址 | 显示并一键复制远程仓库 URL |
686
686
  | 自动刷新 | 窗口获得焦点、标签页重新可见,或从 Activity Bar 切回 **Git** 视图时,自动静默刷新文件状态与分支信息 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zen-gitsync",
3
- "version": "2.16.28",
3
+ "version": "2.16.30",
4
4
  "description": "Auto commit, scheduled sync, and visual GUI for Git. Run `g` in any repo for one-key commit & push, AI commit messages, scheduled background sync, and a drag-and-drop workflow builder.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -1,200 +1,200 @@
1
- // Copyright 2026 xz333221
2
- //
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
14
- //
15
- // g ai 智能体的平台兼容性守卫 — 拦截"在当前 shell 里注定失败"的命令。
16
- //
17
- // 设计背景:
18
- // Windows cmd.exe 没有 tail/head/grep/ls/sed/cat 等 Unix 命令,模型(尤其
19
- // 训练数据偏 Unix 的模型)经常写出 `npm test | tail -60` 这类管道,在
20
- // Windows 上直接报"不是内部或外部命令"。虽然 system prompt 已经提醒过,
21
- // 但模型仍会忽略 —— 本模块在运行时做最后一道防线。
22
- //
23
- // 与 safety.js 的区别:
24
- // - safety.js 拦截"会把系统搞崩"的命令(硬红线,所有平台)
25
- // - 本模块拦截"在当前平台注定失败"的命令(软红线,仅检测 + 给替代方案)
26
- // - 两者互补:safety.js 保命,platformGuard 省时间
27
- //
28
- // 避免误杀策略:
29
- // 不靠命令名硬判,而是先 `where <cmd>` 检查命令是否真的存在于 PATH 中
30
- // (很多 Windows 开发者装了 Git Bash,tail/grep 等确实可用)。只有确认
31
- // 不存在时才拦截。检查结果按命令名缓存,同一会话内不重复查。
32
-
33
- import { exec } from 'node:child_process'
34
-
35
- // ──────────────────────────────────────────────
36
- // Unix-only 命令清单 + 跨平台替代方案
37
- // ──────────────────────────────────────────────
38
- // key = 命令名(小写),value = 拦截时回喂给模型的替代方案说明。
39
- // 只收录"Windows cmd.exe 几乎肯定没有"的命令;像 sort/find 这类 Windows
40
- // 自带的(虽然行为不同)不收录,避免误判。
41
- export const UNIX_ONLY_COMMANDS = {
42
- tail: 'Windows cmd 没有 tail。查看输出末尾:用 PowerShell "命令 | Select-Object -Last 60";查看文件末尾用 read_file 工具(传 offset 靠后)',
43
- head: 'Windows cmd 没有 head。查看输出开头:直接运行命令即可;查看文件开头用 read_file 工具(offset=1)',
44
- cat: 'Windows cmd 没有 cat。查看文件用 read_file 工具,或 cmd 的 type 命令',
45
- grep: 'Windows cmd 没有 grep。搜内容用 search_text 工具,或 cmd 的 findstr 命令',
46
- ls: 'Windows cmd 没有 ls。列目录用 list_files 工具,或 cmd 的 dir 命令',
47
- sed: 'Windows cmd 没有 sed。用 node -e "..." 或 PowerShell 做文本替换',
48
- awk: 'Windows cmd 没有 awk。用 node -e "..." 或 PowerShell 处理文本',
49
- wc: 'Windows cmd 没有 wc。用 node -e "..." 或 PowerShell 统计行数/字数',
50
- cut: 'Windows cmd 没有 cut。用 node -e "..." 或 PowerShell 截取字段',
51
- uniq: 'Windows cmd 没有 uniq。用 node -e "..." 或 PowerShell 去重',
52
- xargs: 'Windows cmd 没有 xargs。用 PowerShell ForEach-Object,或改写命令逻辑',
53
- which: 'Windows cmd 没有 which。用 cmd 的 where 命令查找可执行文件',
54
- touch: 'Windows cmd 没有 touch。用 node -e "require(\'fs\').writeFileSync(\'file\',\'\')" 或 PowerShell "New-Item -Type File"',
55
- }
56
-
57
- // ──────────────────────────────────────────────
58
- // 纯函数:命令段解析(便于单测)
59
- // ──────────────────────────────────────────────
60
-
61
- /**
62
- * 将命令串按管道/链式操作符切分为段。
63
- * 与 safety.js 的 splitCommandSegments 逻辑一致,但独立维护(两个模块各有侧重)。
64
- * @param {string} cmd
65
- * @returns {string[]} 去空白后的命令段
66
- */
67
- export function splitCommandSegments(cmd) {
68
- return String(cmd).split(/&&|\|\||[;|]/).map(s => s.trim()).filter(Boolean)
69
- }
70
-
71
- /**
72
- * 从一个命令段中提取命令名(第一个 token,去掉 sudo/doas 前缀)。
73
- * @param {string} seg
74
- * @returns {string} 小写命令名,可能为空
75
- */
76
- export function extractCommandName(seg) {
77
- const m = String(seg).match(/^(?:sudo|doas)\s+(\S+)|^(\S+)/)
78
- const name = m?.[1] || m?.[2] || ''
79
- return name.toLowerCase()
80
- }
81
-
82
- /**
83
- * 检测命令串中出现的 Unix-only 命令(纯函数,不查 PATH)。
84
- *
85
- * 遍历所有段(管道/链式),返回检测到的 Unix-only 命令名列表。
86
- * 调用方(如 guardCommand)再决定是否需要查 PATH 确认。
87
- *
88
- * @param {string} command - 完整命令行
89
- * @returns {string[]} 检测到的 Unix-only 命令名(去重,保持出现顺序)
90
- */
91
- export function detectUnixOnlyCommands(command) {
92
- if (typeof command !== 'string' || !command.trim()) return []
93
- const found = []
94
- const seen = new Set()
95
- for (const seg of splitCommandSegments(command)) {
96
- const name = extractCommandName(seg)
97
- // 去掉可能的路径前缀(如 /usr/bin/tail → tail)
98
- const base = name.split(/[/\\]/).pop()
99
- if (UNIX_ONLY_COMMANDS[base] && !seen.has(base)) {
100
- seen.add(base)
101
- found.push(base)
102
- }
103
- }
104
- return found
105
- }
106
-
107
- // ──────────────────────────────────────────────
108
- // 命令可用性检查(带缓存,仅 Windows 用)
109
- // ──────────────────────────────────────────────
110
-
111
- // 缓存:命令名 → true(可用)/ false(不可用)。同一进程内 PATH 不变,缓存安全。
112
- const _availabilityCache = new Map()
113
-
114
- /**
115
- * 清空可用性缓存(测试用)。
116
- */
117
- export function _clearAvailabilityCache() {
118
- _availabilityCache.clear()
119
- }
120
-
121
- /**
122
- * 在 Windows 上用 `where <cmd>` 检查命令是否存在于 PATH 中。
123
- *
124
- * @param {string} cmdName - 命令名(如 "tail")
125
- * @param {object} [opts]
126
- * @param {(cmd: string, opts: object, cb: (err, stdout) => void) => void} [opts.execFn]
127
- * 可注入的 exec 函数(测试用),默认用 child_process.exec
128
- * @returns {Promise<boolean>} true = 命令可用;false = 不存在
129
- */
130
- export async function checkCommandAvailability(cmdName, { execFn } = {}) {
131
- if (_availabilityCache.has(cmdName)) return _availabilityCache.get(cmdName)
132
- const _exec = execFn || exec
133
- return new Promise((resolve) => {
134
- _exec(`where ${cmdName}`, { windowsHide: true }, (err) => {
135
- const available = !err
136
- _availabilityCache.set(cmdName, available)
137
- resolve(available)
138
- })
139
- })
140
- }
141
-
142
- // ──────────────────────────────────────────────
143
- // 主接口:平台守卫
144
- // ──────────────────────────────────────────────
145
-
146
- /**
147
- * 检查一条命令在当前平台是否会因命令不存在而注定失败。
148
- *
149
- * 仅在 Windows 上生效:检测命令中的 Unix-only 命令,逐个用 `where` 确认
150
- * 是否可用;不可用的直接拦截并给出跨平台替代方案。
151
- *
152
- * 非 Windows 平台直接放行(Unix 命令天然可用)。
153
- *
154
- * @param {string} command - 待检查的完整命令行
155
- * @param {object} [opts]
156
- * @param {string} [opts.platform=process.platform] - 平台标识(测试用)
157
- * @param {(cmd: string, opts: object, cb: (err, stdout) => void) => void} [opts.execFn]
158
- * 可注入的 exec 函数(测试用)
159
- * @returns {Promise<{ blocked: boolean, reason: string|null }>}
160
- * blocked=true 时 reason 给出中文原因 + 替代方案(会回喂给模型)
161
- */
162
- export async function guardCommand(command, { platform = process.platform, execFn } = {}) {
163
- if (typeof command !== 'string' || !command.trim()) {
164
- return { blocked: false, reason: null }
165
- }
166
- // 非 Windows 平台:Unix 命令天然可用,不需要守卫
167
- if (platform !== 'win32') {
168
- return { blocked: false, reason: null }
169
- }
170
-
171
- const detected = detectUnixOnlyCommands(command)
172
- if (detected.length === 0) {
173
- return { blocked: false, reason: null }
174
- }
175
-
176
- // 逐个检查检测到的 Unix-only 命令是否真的不存在于 PATH
177
- const missing = []
178
- for (const cmdName of detected) {
179
- const available = await checkCommandAvailability(cmdName, { execFn })
180
- if (!available) missing.push(cmdName)
181
- }
182
-
183
- if (missing.length === 0) {
184
- return { blocked: false, reason: null }
185
- }
186
-
187
- // 构建拦截原因:列出每个缺失命令的替代方案
188
- const hints = missing.map(name => ` • ${name}: ${UNIX_ONLY_COMMANDS[name]}`)
189
- const reason = [
190
- `当前 Windows 环境(cmd.exe)中不存在以下 Unix 命令,执行会失败:`,
191
- ...hints,
192
- '',
193
- `请改用上述跨平台方案或内置工具,然后重试。`,
194
- `原始命令: ${command}`,
195
- ].join('\n')
196
-
197
- return { blocked: true, reason }
198
- }
199
-
200
- export default { guardCommand, detectUnixOnlyCommands, checkCommandAvailability, UNIX_ONLY_COMMANDS }
1
+ // Copyright 2026 xz333221
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+ //
15
+ // g ai 智能体的平台兼容性守卫 — 拦截"在当前 shell 里注定失败"的命令。
16
+ //
17
+ // 设计背景:
18
+ // Windows cmd.exe 没有 tail/head/grep/ls/sed/cat 等 Unix 命令,模型(尤其
19
+ // 训练数据偏 Unix 的模型)经常写出 `npm test | tail -60` 这类管道,在
20
+ // Windows 上直接报"不是内部或外部命令"。虽然 system prompt 已经提醒过,
21
+ // 但模型仍会忽略 —— 本模块在运行时做最后一道防线。
22
+ //
23
+ // 与 safety.js 的区别:
24
+ // - safety.js 拦截"会把系统搞崩"的命令(硬红线,所有平台)
25
+ // - 本模块拦截"在当前平台注定失败"的命令(软红线,仅检测 + 给替代方案)
26
+ // - 两者互补:safety.js 保命,platformGuard 省时间
27
+ //
28
+ // 避免误杀策略:
29
+ // 不靠命令名硬判,而是先 `where <cmd>` 检查命令是否真的存在于 PATH 中
30
+ // (很多 Windows 开发者装了 Git Bash,tail/grep 等确实可用)。只有确认
31
+ // 不存在时才拦截。检查结果按命令名缓存,同一会话内不重复查。
32
+
33
+ import { exec } from 'node:child_process'
34
+
35
+ // ──────────────────────────────────────────────
36
+ // Unix-only 命令清单 + 跨平台替代方案
37
+ // ──────────────────────────────────────────────
38
+ // key = 命令名(小写),value = 拦截时回喂给模型的替代方案说明。
39
+ // 只收录"Windows cmd.exe 几乎肯定没有"的命令;像 sort/find 这类 Windows
40
+ // 自带的(虽然行为不同)不收录,避免误判。
41
+ export const UNIX_ONLY_COMMANDS = {
42
+ tail: 'Windows cmd 没有 tail。查看输出末尾:用 PowerShell "命令 | Select-Object -Last 60";查看文件末尾用 read_file 工具(传 offset 靠后)',
43
+ head: 'Windows cmd 没有 head。查看输出开头:直接运行命令即可;查看文件开头用 read_file 工具(offset=1)',
44
+ cat: 'Windows cmd 没有 cat。查看文件用 read_file 工具,或 cmd 的 type 命令',
45
+ grep: 'Windows cmd 没有 grep。搜内容用 search_text 工具,或 cmd 的 findstr 命令',
46
+ ls: 'Windows cmd 没有 ls。列目录用 list_files 工具,或 cmd 的 dir 命令',
47
+ sed: 'Windows cmd 没有 sed。用 node -e "..." 或 PowerShell 做文本替换',
48
+ awk: 'Windows cmd 没有 awk。用 node -e "..." 或 PowerShell 处理文本',
49
+ wc: 'Windows cmd 没有 wc。用 node -e "..." 或 PowerShell 统计行数/字数',
50
+ cut: 'Windows cmd 没有 cut。用 node -e "..." 或 PowerShell 截取字段',
51
+ uniq: 'Windows cmd 没有 uniq。用 node -e "..." 或 PowerShell 去重',
52
+ xargs: 'Windows cmd 没有 xargs。用 PowerShell ForEach-Object,或改写命令逻辑',
53
+ which: 'Windows cmd 没有 which。用 cmd 的 where 命令查找可执行文件',
54
+ touch: 'Windows cmd 没有 touch。用 node -e "require(\'fs\').writeFileSync(\'file\',\'\')" 或 PowerShell "New-Item -Type File"',
55
+ }
56
+
57
+ // ──────────────────────────────────────────────
58
+ // 纯函数:命令段解析(便于单测)
59
+ // ──────────────────────────────────────────────
60
+
61
+ /**
62
+ * 将命令串按管道/链式操作符切分为段。
63
+ * 与 safety.js 的 splitCommandSegments 逻辑一致,但独立维护(两个模块各有侧重)。
64
+ * @param {string} cmd
65
+ * @returns {string[]} 去空白后的命令段
66
+ */
67
+ export function splitCommandSegments(cmd) {
68
+ return String(cmd).split(/&&|\|\||[;|]/).map(s => s.trim()).filter(Boolean)
69
+ }
70
+
71
+ /**
72
+ * 从一个命令段中提取命令名(第一个 token,去掉 sudo/doas 前缀)。
73
+ * @param {string} seg
74
+ * @returns {string} 小写命令名,可能为空
75
+ */
76
+ export function extractCommandName(seg) {
77
+ const m = String(seg).match(/^(?:sudo|doas)\s+(\S+)|^(\S+)/)
78
+ const name = m?.[1] || m?.[2] || ''
79
+ return name.toLowerCase()
80
+ }
81
+
82
+ /**
83
+ * 检测命令串中出现的 Unix-only 命令(纯函数,不查 PATH)。
84
+ *
85
+ * 遍历所有段(管道/链式),返回检测到的 Unix-only 命令名列表。
86
+ * 调用方(如 guardCommand)再决定是否需要查 PATH 确认。
87
+ *
88
+ * @param {string} command - 完整命令行
89
+ * @returns {string[]} 检测到的 Unix-only 命令名(去重,保持出现顺序)
90
+ */
91
+ export function detectUnixOnlyCommands(command) {
92
+ if (typeof command !== 'string' || !command.trim()) return []
93
+ const found = []
94
+ const seen = new Set()
95
+ for (const seg of splitCommandSegments(command)) {
96
+ const name = extractCommandName(seg)
97
+ // 去掉可能的路径前缀(如 /usr/bin/tail → tail)
98
+ const base = name.split(/[/\\]/).pop()
99
+ if (UNIX_ONLY_COMMANDS[base] && !seen.has(base)) {
100
+ seen.add(base)
101
+ found.push(base)
102
+ }
103
+ }
104
+ return found
105
+ }
106
+
107
+ // ──────────────────────────────────────────────
108
+ // 命令可用性检查(带缓存,仅 Windows 用)
109
+ // ──────────────────────────────────────────────
110
+
111
+ // 缓存:命令名 → true(可用)/ false(不可用)。同一进程内 PATH 不变,缓存安全。
112
+ const _availabilityCache = new Map()
113
+
114
+ /**
115
+ * 清空可用性缓存(测试用)。
116
+ */
117
+ export function _clearAvailabilityCache() {
118
+ _availabilityCache.clear()
119
+ }
120
+
121
+ /**
122
+ * 在 Windows 上用 `where <cmd>` 检查命令是否存在于 PATH 中。
123
+ *
124
+ * @param {string} cmdName - 命令名(如 "tail")
125
+ * @param {object} [opts]
126
+ * @param {(cmd: string, opts: object, cb: (err, stdout) => void) => void} [opts.execFn]
127
+ * 可注入的 exec 函数(测试用),默认用 child_process.exec
128
+ * @returns {Promise<boolean>} true = 命令可用;false = 不存在
129
+ */
130
+ export async function checkCommandAvailability(cmdName, { execFn } = {}) {
131
+ if (_availabilityCache.has(cmdName)) return _availabilityCache.get(cmdName)
132
+ const _exec = execFn || exec
133
+ return new Promise((resolve) => {
134
+ _exec(`where ${cmdName}`, { windowsHide: true }, (err) => {
135
+ const available = !err
136
+ _availabilityCache.set(cmdName, available)
137
+ resolve(available)
138
+ })
139
+ })
140
+ }
141
+
142
+ // ──────────────────────────────────────────────
143
+ // 主接口:平台守卫
144
+ // ──────────────────────────────────────────────
145
+
146
+ /**
147
+ * 检查一条命令在当前平台是否会因命令不存在而注定失败。
148
+ *
149
+ * 仅在 Windows 上生效:检测命令中的 Unix-only 命令,逐个用 `where` 确认
150
+ * 是否可用;不可用的直接拦截并给出跨平台替代方案。
151
+ *
152
+ * 非 Windows 平台直接放行(Unix 命令天然可用)。
153
+ *
154
+ * @param {string} command - 待检查的完整命令行
155
+ * @param {object} [opts]
156
+ * @param {string} [opts.platform=process.platform] - 平台标识(测试用)
157
+ * @param {(cmd: string, opts: object, cb: (err, stdout) => void) => void} [opts.execFn]
158
+ * 可注入的 exec 函数(测试用)
159
+ * @returns {Promise<{ blocked: boolean, reason: string|null }>}
160
+ * blocked=true 时 reason 给出中文原因 + 替代方案(会回喂给模型)
161
+ */
162
+ export async function guardCommand(command, { platform = process.platform, execFn } = {}) {
163
+ if (typeof command !== 'string' || !command.trim()) {
164
+ return { blocked: false, reason: null }
165
+ }
166
+ // 非 Windows 平台:Unix 命令天然可用,不需要守卫
167
+ if (platform !== 'win32') {
168
+ return { blocked: false, reason: null }
169
+ }
170
+
171
+ const detected = detectUnixOnlyCommands(command)
172
+ if (detected.length === 0) {
173
+ return { blocked: false, reason: null }
174
+ }
175
+
176
+ // 逐个检查检测到的 Unix-only 命令是否真的不存在于 PATH
177
+ const missing = []
178
+ for (const cmdName of detected) {
179
+ const available = await checkCommandAvailability(cmdName, { execFn })
180
+ if (!available) missing.push(cmdName)
181
+ }
182
+
183
+ if (missing.length === 0) {
184
+ return { blocked: false, reason: null }
185
+ }
186
+
187
+ // 构建拦截原因:列出每个缺失命令的替代方案
188
+ const hints = missing.map(name => ` • ${name}: ${UNIX_ONLY_COMMANDS[name]}`)
189
+ const reason = [
190
+ `当前 Windows 环境(cmd.exe)中不存在以下 Unix 命令,执行会失败:`,
191
+ ...hints,
192
+ '',
193
+ `请改用上述跨平台方案或内置工具,然后重试。`,
194
+ `原始命令: ${command}`,
195
+ ].join('\n')
196
+
197
+ return { blocked: true, reason }
198
+ }
199
+
200
+ export default { guardCommand, detectUnixOnlyCommands, checkCommandAvailability, UNIX_ONLY_COMMANDS }