zen-gitsync 2.16.6 → 2.16.8

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 (114) hide show
  1. package/package.json +1 -1
  2. package/src/cli/ai/modelSetup.js +594 -594
  3. package/src/cli/ai/modelSetup.test.js +496 -496
  4. package/src/cli/ai/platformGuard.js +200 -200
  5. package/src/cli/ai/platformGuard.test.js +253 -253
  6. package/src/gitCommit.js +484 -484
  7. package/src/ui/public/assets/AgentView-8-xsOmE6.css +1 -0
  8. package/src/ui/public/assets/AgentView-DSmUynN-.js +4 -0
  9. package/src/ui/public/assets/AppVersionBadge-CS7Trvp-.css +1 -0
  10. package/src/ui/public/assets/AppVersionBadge-CcFagCnq.js +2 -0
  11. package/src/ui/public/assets/BranchSelector-B0eLkAdv.css +1 -0
  12. package/src/ui/public/assets/BranchSelector-DcYVBQJh.js +1 -0
  13. package/src/ui/public/assets/CommitForm-CkTI8Xyz.js +6 -0
  14. package/src/ui/public/assets/CommitForm-DdK8dymc.css +1 -0
  15. package/src/ui/public/assets/CommonDialog-BFyhw0Qj.js +1 -0
  16. package/src/ui/public/assets/CommonDialog-ChSKsEz2.css +1 -0
  17. package/src/ui/public/assets/EditorView-BVod-rjn.css +1 -0
  18. package/src/ui/public/assets/EditorView-DuHkBSc3.js +0 -0
  19. package/src/ui/public/assets/FlowExecutionViewer-C063Qjmk.js +1 -0
  20. package/src/ui/public/assets/FlowExecutionViewer-jNHI5zdm.css +1 -0
  21. package/src/ui/public/assets/FlowOrchestrationWorkspace-BeJRId5G.js +21 -0
  22. package/src/ui/public/assets/FlowOrchestrationWorkspace-CYGp-PqS.css +1 -0
  23. package/src/ui/public/assets/LogList-CeFFytjB.css +1 -0
  24. package/src/ui/public/assets/LogList-GSeER4cv.js +15 -0
  25. package/src/ui/public/assets/MindmapView-BQTqx842.css +1 -0
  26. package/src/ui/public/assets/MindmapView-p7szLCtQ.js +2 -0
  27. package/src/ui/public/assets/MonitorView-Be6KEUCe.js +1 -0
  28. package/src/ui/public/assets/MonitorView-TaS4kL0Y.css +1 -0
  29. package/src/ui/public/assets/ProjectStartupButton-F1k5Km2p.js +1 -0
  30. package/src/ui/public/assets/ProjectStartupButton-QLPltCK5.css +1 -0
  31. package/src/ui/public/assets/RemoteRepoCard-BRGNlbeO.css +1 -0
  32. package/src/ui/public/assets/RemoteRepoCard-CSMFlWDl.js +5 -0
  33. package/src/ui/public/assets/SourceMapView-BNRjvgzH.css +1 -0
  34. package/src/ui/public/assets/SourceMapView-Dg5NP_gR.js +3 -0
  35. package/src/ui/public/assets/{SvgIcon-BQ6bR7l0.css → SvgIcon-CDNsk4-Q.css} +1 -1
  36. package/src/ui/public/assets/SvgIcon-CYZGHQAE.js +1 -0
  37. package/src/ui/public/assets/UserInputNode-3OAXiGVh.js +1 -0
  38. package/src/ui/public/assets/UserInputNode-BVgQkB2l.css +1 -0
  39. package/src/ui/public/assets/WorkbenchView-8xyxdWAg.js +28 -0
  40. package/src/ui/public/assets/{WorkbenchView-CQfjF0Gr.css → WorkbenchView-DbF4hH5s.css} +1 -1
  41. package/src/ui/public/assets/_plugin-vue_export-helper-DgLDQTWK.js +15 -0
  42. package/src/ui/public/assets/{configStore-CmG77GNj.js → configStore-D5t05s4G.js} +1 -1
  43. package/src/ui/public/assets/element-plus-CyGhXd7X.js +25 -0
  44. package/src/ui/public/assets/element-plus-D3Dn7nX0.css +1 -0
  45. package/src/ui/public/assets/{flow-mindmap-D26cYDAD.js → flow-mindmap-CUvJaYn2.js} +1 -1
  46. package/src/ui/public/assets/index--onqtBRQ.css +1 -0
  47. package/src/ui/public/assets/index-DDGvh1mp.js +68 -0
  48. package/src/ui/public/assets/socket-io-BSgn8Jx2.js +1 -0
  49. package/src/ui/public/assets/vendor-DVvq7qtS.css +1 -0
  50. package/src/ui/public/assets/{vendor-lXqgobGS.js → vendor-jF--I87-.js} +88 -37
  51. package/src/ui/public/assets/vue-flow-CWmfpfPN.js +39 -0
  52. package/src/ui/public/favicon.svg +75 -75
  53. package/src/ui/public/index.html +31 -31
  54. package/src/ui/public/logo.svg +74 -74
  55. package/src/ui/server/middleware/requestLogger.js +53 -53
  56. package/src/ui/server/routes/branchStatus.js +105 -105
  57. package/src/ui/server/routes/fileOpen.js +3 -2
  58. package/src/ui/server/routes/fs.js +791 -791
  59. package/src/ui/server/routes/git/diff.js +426 -426
  60. package/src/ui/server/routes/git/diffUtils.js +149 -149
  61. package/src/ui/server/routes/git/stash.js +550 -550
  62. package/src/ui/server/routes/git/tags.js +176 -176
  63. package/src/ui/server/routes/git.js +181 -181
  64. package/src/ui/server/routes/gitOps.js +1255 -1255
  65. package/src/ui/server/routes/process.js +86 -86
  66. package/src/ui/server/routes/status.js +67 -67
  67. package/src/ui/server/routes/workbench/pdfText.js +145 -145
  68. package/src/ui/server/utils/createSavePortToFile.js +48 -48
  69. package/src/ui/server/utils/instanceRegistry.js +282 -282
  70. package/src/ui/server/utils/startServerOnAvailablePort.js +116 -116
  71. package/src/utils/index.js +1173 -1173
  72. package/src/ui/public/assets/AgentView-D3ltcW16.js +0 -4
  73. package/src/ui/public/assets/AgentView-DJBC9U8b.css +0 -1
  74. package/src/ui/public/assets/AppVersionBadge-DEzojWz9.js +0 -2
  75. package/src/ui/public/assets/AppVersionBadge-Dj6H729N.css +0 -1
  76. package/src/ui/public/assets/BranchSelector-T_rJm-JY.css +0 -1
  77. package/src/ui/public/assets/BranchSelector-YgVQRusR.js +0 -1
  78. package/src/ui/public/assets/CommitForm-B9xoh9cE.js +0 -6
  79. package/src/ui/public/assets/CommitForm-CUvl0VcF.css +0 -1
  80. package/src/ui/public/assets/CommonDialog-Cyh3xu0p.css +0 -1
  81. package/src/ui/public/assets/CommonDialog-DB4V4CKn.js +0 -1
  82. package/src/ui/public/assets/EditorView-CRYGVw_r.css +0 -1
  83. package/src/ui/public/assets/EditorView-CsjhdSd_.js +0 -0
  84. package/src/ui/public/assets/FlowExecutionViewer-B5kh7VxS.css +0 -1
  85. package/src/ui/public/assets/FlowExecutionViewer-Blax8J71.js +0 -1
  86. package/src/ui/public/assets/FlowOrchestrationWorkspace-BAQDU9Jd.css +0 -1
  87. package/src/ui/public/assets/FlowOrchestrationWorkspace-Dm85yYLO.js +0 -21
  88. package/src/ui/public/assets/LogList-DJYpDUTZ.css +0 -1
  89. package/src/ui/public/assets/LogList-WxAgsMg0.js +0 -7
  90. package/src/ui/public/assets/MindmapView-CS2t30Ko.css +0 -1
  91. package/src/ui/public/assets/MindmapView-_FKSYPEk.js +0 -1
  92. package/src/ui/public/assets/MonitorView-CynFYBMr.js +0 -1
  93. package/src/ui/public/assets/MonitorView-DxFogQIt.css +0 -1
  94. package/src/ui/public/assets/ProjectStartupButton-B5vk2Lu0.js +0 -1
  95. package/src/ui/public/assets/ProjectStartupButton-B7zvqRj_.css +0 -1
  96. package/src/ui/public/assets/RemoteRepoCard-B_iW7EZK.js +0 -1
  97. package/src/ui/public/assets/RemoteRepoCard-d0klonVb.css +0 -1
  98. package/src/ui/public/assets/SourceMapView-0fHVZr1r.js +0 -3
  99. package/src/ui/public/assets/SourceMapView-DG_a_076.css +0 -1
  100. package/src/ui/public/assets/SvgIcon-Ch_wHlf9.js +0 -1
  101. package/src/ui/public/assets/UserInputNode-DDtvv9Iq.css +0 -1
  102. package/src/ui/public/assets/UserInputNode-LbWvYQcA.js +0 -1
  103. package/src/ui/public/assets/WorkbenchView-CYucXDsF.js +0 -12
  104. package/src/ui/public/assets/_plugin-vue_export-helper-7QfI9jF6.js +0 -15
  105. package/src/ui/public/assets/ai-chat-ui-BqgKQovY.js +0 -45
  106. package/src/ui/public/assets/ai-chat-ui-BwlJovCq.css +0 -1
  107. package/src/ui/public/assets/element-plus-CQrjg9us.css +0 -1
  108. package/src/ui/public/assets/element-plus-DjpeTxOl.js +0 -24
  109. package/src/ui/public/assets/index-CmQSuclp.js +0 -38
  110. package/src/ui/public/assets/index-hGb6cPd0.css +0 -1
  111. package/src/ui/public/assets/socket-io-z4n873W1.js +0 -1
  112. package/src/ui/public/assets/vendor-CdOLCu7-.css +0 -1
  113. package/src/ui/public/assets/vue-flow-CIbkTMWF.js +0 -39
  114. package/src/ui/server/.claude/codediff.txt +0 -8
@@ -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 }