zen-gitsync 2.17.8 → 2.17.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/package.json +2 -1
- package/src/cli/ai/agent.js +210 -58
- package/src/cli/ai/context.js +58 -0
- package/src/cli/ai/context.test.js +117 -0
- package/src/cli/ai/extensions.js +131 -0
- package/src/cli/ai/extensions.test.js +158 -0
- package/src/cli/ai/mcp.js +498 -0
- package/src/cli/ai/mcp.test.js +275 -0
- package/src/cli/ai/runtime.test.js +32 -0
- package/src/cli/ai/skills.js +207 -0
- package/src/cli/ai/skills.test.js +126 -0
- package/src/cli/ai/termui.js +1 -0
- package/src/cli/ai/tools.js +94 -3
- package/src/cli/ai/tools.test.js +82 -0
- package/src/cli/ai/transport.js +18 -4
- package/src/cli/ai/turn.js +21 -5
- package/src/config.js +27 -3
- package/src/paths.js +11 -0
- package/src/ui/public/assets/AgentView-BV15UtEF.css +1 -0
- package/src/ui/public/assets/AgentView-C-AJo7KS.js +5 -0
- package/src/ui/public/assets/AppVersionBadge-B6R6eN2N.js +2 -0
- package/src/ui/public/assets/BranchSelector-Cj3kMWON.js +1 -0
- package/src/ui/public/assets/CommitForm-DpXuFG-F.js +6 -0
- package/src/ui/public/assets/CommonDialog-BLPHDsn0.js +1 -0
- package/src/ui/public/assets/EditorView-B1OnoXXf.js +1 -0
- package/src/ui/public/assets/FileDiffViewer-BQl0JcWd.js +14 -0
- package/src/ui/public/assets/{FlowExecutionViewer-uXrzZjsW.js → FlowExecutionViewer-BgEt5_Hz.js} +1 -1
- package/src/ui/public/assets/FlowOrchestrationWorkspace-CbLmsSF8.js +21 -0
- package/src/ui/public/assets/IconButton-aM3vaSIN.js +1 -0
- package/src/ui/public/assets/LogList-UjoVFZ-H.js +7 -0
- package/src/ui/public/assets/MindmapView-D_04v0n4.js +1 -0
- package/src/ui/public/assets/MonitorView-BD1_uzq7.js +1 -0
- package/src/ui/public/assets/ProjectStartupButton-BTYlVvzh.js +1 -0
- package/src/ui/public/assets/RemoteManagerDialog-20mpQMwu.js +1 -0
- package/src/ui/public/assets/RemoteRepoCard-DOrm17Xd.js +1 -0
- package/src/ui/public/assets/SourceMapView-nUU9bYPh.js +3 -0
- package/src/ui/public/assets/SvgIcon-LUynX3Iq.js +1 -0
- package/src/ui/public/assets/TemplateManager-Bo3Q7J9Y.js +1 -0
- package/src/ui/public/assets/UserInputNode-D5tSXiJz.js +1 -0
- package/src/ui/public/assets/{WorkbenchView-v3QFn6Ln.css → WorkbenchView-C_Gmzvj1.css} +1 -1
- package/src/ui/public/assets/WorkbenchView-UGC334JL.js +13 -0
- package/src/ui/public/assets/_plugin-vue_export-helper-D-D9qkRz.js +19 -0
- package/src/ui/public/assets/configStore-BLRcYJdf.js +1 -0
- package/src/ui/public/assets/{element-plus-KTmRV-so.js → element-plus-DcZ-TVp6.js} +3 -3
- package/src/ui/public/assets/flow-mindmap-D1VTkZh3.js +139 -0
- package/src/ui/public/assets/{gitStore-CpcS-MzM.js → gitStore-DMEDMZMk.js} +4 -4
- package/src/ui/public/assets/index-CW3xs-af.css +1 -0
- package/src/ui/public/assets/index-S0_gRyiO.js +26 -0
- package/src/ui/public/assets/{office-docx-BfSBpdBv.js → office-docx-C0g5Oepv.js} +5 -5
- package/src/ui/public/assets/{office-excel-S95W2mv4.js → office-excel-D-uYLKaA.js} +1 -1
- package/src/ui/public/assets/{office-pptx-DFFarGrk.js → office-pptx-De3fnc6M.js} +1 -1
- package/src/ui/public/assets/previewSandbox-DXAxqDVu.js +0 -0
- package/src/ui/public/assets/toolsStore-CpImRUhI.js +1 -0
- package/src/ui/public/assets/{useThemeObserver-B01D-Vl6.js → useThemeObserver-CG8k8U18.js} +1 -1
- package/src/ui/public/assets/{vendor-dO4B7wQf.js → vendor-BIgZEtkx.js} +27 -27
- package/src/ui/public/assets/vue-flow-CPCFgenl.js +39 -0
- package/src/ui/public/index.html +17 -17
- package/src/ui/server/index.js +4 -0
- package/src/ui/server/routes/config.js +16 -3
- package/src/ui/server/routes/exec.js +19 -11
- package/src/ui/server/routes/fileOpen.js +142 -2
- package/src/ui/server/routes/remoteRepos.js +604 -0
- package/src/ui/server/routes/remoteRepos.test.js +386 -0
- package/src/ui/server/routes/workbench/agentChat.js +86 -180
- package/src/ui/server/routes/workbench/agentChatShared.test.js +151 -0
- package/src/ui/server/routes/workbench/agentMarketplace.js +986 -0
- package/src/ui/server/routes/workbench/agentMarketplace.test.js +306 -0
- package/src/ui/server/routes/workbench/agentParity.test.js +167 -0
- package/src/ui/server/routes/workbench/agentRoutes.js +86 -1
- package/src/ui/server/routes/workbench/agentRoutes.test.js +50 -1
- package/src/ui/server/routes/workbench/jobStore.js +17 -3
- package/src/ui/server/routes/workbench/orchestratorStore.js +42 -2
- package/src/ui/server/routes/workbench/projectRegistry.js +6 -0
- package/src/ui/server/routes/workbench/projectRegistry.test.js +70 -0
- package/src/ui/server/routes/workbench/projectTool.js +198 -0
- package/src/ui/server/routes/workbench/projectTool.test.js +152 -0
- package/src/ui/server/routes/workbench/remoteCliPromptParity.test.js +177 -0
- package/src/ui/server/routes/workbench/taskRunner.js +269 -12
- package/src/ui/server/routes/workbench/taskRunner.toolcalls.test.js +307 -0
- package/src/ui/server/socket/registerUiSocketHandlers.js +18 -11
- package/src/utils/shellPath.js +253 -0
- package/src/utils/shellPath.test.js +221 -0
- package/src/ui/public/assets/AgentView-CdJaeRh0.js +0 -5
- package/src/ui/public/assets/AgentView-DAY4S5WN.css +0 -1
- package/src/ui/public/assets/AppVersionBadge-CiijSYfM.js +0 -2
- package/src/ui/public/assets/BranchSelector-BS7ajwvv.js +0 -1
- package/src/ui/public/assets/CommitForm-CEOSqXCA.js +0 -6
- package/src/ui/public/assets/CommonDialog-CAaEi-4T.js +0 -1
- package/src/ui/public/assets/EditorView-DyaFyRDD.js +0 -1
- package/src/ui/public/assets/FileDiffViewer-BvX9cQjR.js +0 -14
- package/src/ui/public/assets/FlowOrchestrationWorkspace-DIBpUSt_.js +0 -21
- package/src/ui/public/assets/IconButton-CnoW2on6.js +0 -1
- package/src/ui/public/assets/LogList-D2espSVo.js +0 -7
- package/src/ui/public/assets/MindmapView-CZF-rZVs.js +0 -1
- package/src/ui/public/assets/MonitorView-DjLr7qOG.js +0 -1
- package/src/ui/public/assets/ProjectStartupButton-DzsU-QIO.js +0 -1
- package/src/ui/public/assets/RemoteManagerDialog-CuuG2ucT.js +0 -1
- package/src/ui/public/assets/RemoteRepoCard-BGf8L0of.js +0 -1
- package/src/ui/public/assets/SourceMapView-CFb02m6z.js +0 -3
- package/src/ui/public/assets/SvgIcon-SRAeYjnN.js +0 -1
- package/src/ui/public/assets/TemplateManager-gedqAXl8.js +0 -1
- package/src/ui/public/assets/UserInputNode-BOCuMQW6.js +0 -1
- package/src/ui/public/assets/WorkbenchView-CRoR21LQ.js +0 -13
- package/src/ui/public/assets/_plugin-vue_export-helper-C8OE7U9H.js +0 -19
- package/src/ui/public/assets/configStore-CuMElTgV.js +0 -1
- package/src/ui/public/assets/flow-mindmap-CurCIiCp.js +0 -139
- package/src/ui/public/assets/index-cApp6w0b.css +0 -1
- package/src/ui/public/assets/index-yXm9JLGv.js +0 -24
- package/src/ui/public/assets/previewSandbox-DkXYEpLP.js +0 -0
- package/src/ui/public/assets/toolsStore-DGxlRe3Y.js +0 -1
- package/src/ui/public/assets/vue-flow-CsvDD38k.js +0 -39
package/src/ui/server/index.js
CHANGED
|
@@ -41,6 +41,7 @@ import { registerGitRoutes } from './routes/git.js';
|
|
|
41
41
|
import { registerFsRoutes } from './routes/fs.js';
|
|
42
42
|
import { registerNpmRoutes } from './routes/npm.js';
|
|
43
43
|
import { registerFileOpenRoutes } from './routes/fileOpen.js';
|
|
44
|
+
import { registerRemoteReposRoutes } from './routes/remoteRepos.js';
|
|
44
45
|
import { registerWorkbenchRoutes } from './routes/workbench.js';
|
|
45
46
|
import { registerGitOpsRoutes } from './routes/gitOps.js';
|
|
46
47
|
import { registerCodeRoutes } from './routes/code.js';
|
|
@@ -404,6 +405,9 @@ async function startUIServer(noOpen = false, savePort = false) {
|
|
|
404
405
|
app
|
|
405
406
|
});
|
|
406
407
|
|
|
408
|
+
// 远程托管平台仓库列表(GitHub / Gitee 两个 Tab 的数据源)
|
|
409
|
+
registerRemoteReposRoutes({ app });
|
|
410
|
+
|
|
407
411
|
registerWorkbenchRoutes({
|
|
408
412
|
app,
|
|
409
413
|
getCurrentProjectPath: () => currentProjectPath,
|
|
@@ -986,10 +986,10 @@ export function registerConfigRoutes({
|
|
|
986
986
|
}
|
|
987
987
|
})
|
|
988
988
|
|
|
989
|
-
//
|
|
989
|
+
// 保存通用设置(主题、语言、工作台任务执行器、任务完成提示)
|
|
990
990
|
app.post('/api/config/save-general-settings', express.json(), async (req, res) => {
|
|
991
991
|
try {
|
|
992
|
-
const { theme, locale, taskExecutor } = req.body
|
|
992
|
+
const { theme, locale, taskExecutor, notifyOnTaskDone } = req.body
|
|
993
993
|
|
|
994
994
|
// 读取原始配置以保留项目设置
|
|
995
995
|
const rawConfig = await configManager.readRawConfigFile()
|
|
@@ -1007,9 +1007,22 @@ export function registerConfigRoutes({
|
|
|
1007
1007
|
rawConfig.taskExecutor = normalizedExecutor
|
|
1008
1008
|
}
|
|
1009
1009
|
|
|
1010
|
+
// 任务执行结束提示开关(全局,默认关)。只接受布尔值,其它类型静默忽略,
|
|
1011
|
+
// 避免前端误传字符串 'false' 被当成真值落盘后永久打开。
|
|
1012
|
+
// ⚠️ 规范化函数对非法值返回的是 **null**(与 normalizeTaskExecutor 同语义),
|
|
1013
|
+
// 不是 undefined —— 判 `!== undefined` 会把非法值原样写进去变成 `null`。
|
|
1014
|
+
const normalizedNotify = configManager.normalizeNotifyOnTaskDone(notifyOnTaskDone)
|
|
1015
|
+
if (normalizedNotify !== null) {
|
|
1016
|
+
rawConfig.notifyOnTaskDone = normalizedNotify
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1010
1019
|
// 直接写入原始配置,避免覆盖项目设置
|
|
1011
1020
|
await configManager.writeRawConfigFile(rawConfig)
|
|
1012
|
-
res.json({
|
|
1021
|
+
res.json({
|
|
1022
|
+
success: true,
|
|
1023
|
+
taskExecutor: normalizedExecutor || undefined,
|
|
1024
|
+
notifyOnTaskDone: normalizedNotify ?? undefined
|
|
1025
|
+
})
|
|
1013
1026
|
} catch (error) {
|
|
1014
1027
|
res.status(500).json({ success: false, error: error.message })
|
|
1015
1028
|
}
|
|
@@ -18,6 +18,7 @@ import iconv from 'iconv-lite';
|
|
|
18
18
|
import { spawn } from 'child_process';
|
|
19
19
|
import { ensureWithinCwd } from '../utils/pathGuard.js';
|
|
20
20
|
import { asyncRoute, HttpError } from '../utils/asyncRoute.js';
|
|
21
|
+
import { augmentEnvPath } from '../../../utils/shellPath.js';
|
|
21
22
|
|
|
22
23
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
23
24
|
// 命令 → argv 拆分(SEC-INJ-2 修复)
|
|
@@ -92,16 +93,23 @@ function resolveBinAndArgs(command) {
|
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
// 通用 env 模板 — 颜色 + 终端兼容性,所有 exec 流式 / 交互式复用
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
96
|
+
//
|
|
97
|
+
// PATH 交给 augmentEnvPath 补一次注册表最新值:服务端进程的环境块是**启动快照**,
|
|
98
|
+
// 用户在这个进程活着期间装的 CLI 不在里面,表现是"GUI 里能探测到、执行却报
|
|
99
|
+
// '不是内部或外部命令'"(2026-09-22 实测 gh)。详见 utils/shellPath.js。
|
|
100
|
+
// 因为要读注册表,这里必须是函数而不能是模块级常量 —— 常量会在 import 时定死。
|
|
101
|
+
async function buildChildEnv() {
|
|
102
|
+
return augmentEnvPath({
|
|
103
|
+
...process.env,
|
|
104
|
+
GIT_CONFIG_PARAMETERS: "'color.ui=always' 'color.status=always' 'core.quotepath=false'",
|
|
105
|
+
FORCE_COLOR: '3',
|
|
106
|
+
NPM_CONFIG_COLOR: 'always',
|
|
107
|
+
TERM: 'xterm-256color',
|
|
108
|
+
COLORTERM: 'truecolor',
|
|
109
|
+
CLICOLOR_FORCE: '1',
|
|
110
|
+
PYTHONUNBUFFERED: '1',
|
|
111
|
+
});
|
|
112
|
+
}
|
|
105
113
|
|
|
106
114
|
function isWindowsBuiltinCommand(command) {
|
|
107
115
|
if (process.platform !== 'win32') return false;
|
|
@@ -157,7 +165,7 @@ export function registerExecRoutes({
|
|
|
157
165
|
// spawn(bin, argv, { shell: false }) — 不走 shell,无注入
|
|
158
166
|
const childProcess = spawn(bin, args, {
|
|
159
167
|
cwd: execDirectory,
|
|
160
|
-
env:
|
|
168
|
+
env: await buildChildEnv(),
|
|
161
169
|
});
|
|
162
170
|
|
|
163
171
|
runningProcesses.set(processId, {
|
|
@@ -20,6 +20,7 @@ import open from 'open';
|
|
|
20
20
|
import { spawn, spawnSync, execFile } from 'child_process';
|
|
21
21
|
import { fileURLToPath } from 'node:url';
|
|
22
22
|
import { promisify } from 'node:util';
|
|
23
|
+
import { augmentEnvPath, pathValueOf, withPathValue } from '../../../utils/shellPath.js';
|
|
23
24
|
|
|
24
25
|
const execFileAsync = promisify(execFile);
|
|
25
26
|
|
|
@@ -38,6 +39,10 @@ const TOOL_DOCS_URLS = Object.freeze({
|
|
|
38
39
|
kimi: 'https://moonshotai.github.io/kimi-code/en/guides/getting-started',
|
|
39
40
|
zcode: 'https://zcode.z.ai/',
|
|
40
41
|
dsh: 'https://github.com/deepseek-ai/deepseek-harness',
|
|
42
|
+
// 远程仓库列表页(RemoteReposList)用到的两个托管平台 CLI。它们不是"AI 工具",
|
|
43
|
+
// 不参与 /api/check-tools 的 open-with 菜单,只借这里的白名单走 /api/install-tool。
|
|
44
|
+
gh: 'https://cli.github.com/',
|
|
45
|
+
gitee: 'https://gitee.com/oschina/gitee-cli',
|
|
41
46
|
});
|
|
42
47
|
|
|
43
48
|
function commandExists(command, platform = process.platform) {
|
|
@@ -251,10 +256,91 @@ export function getToolInstallers(platform = process.platform, hasCommand = comm
|
|
|
251
256
|
updatePackageManager: 'ZCode 官方安装包',
|
|
252
257
|
updateNote: 'ZCode 是桌面应用,升级请到官网下载新版安装包覆盖安装。',
|
|
253
258
|
};
|
|
259
|
+
|
|
260
|
+
// ── 远程托管平台 CLI(远程仓库列表页用)──────────────────────────────────
|
|
261
|
+
// 与上面的 AI 工具共用同一份白名单,好处是"前端只能提交 tool id、命令由服务端
|
|
262
|
+
// 决定"这条安全边界不用再实现一遍。它们不出现在 open-with 菜单里 —— 那个菜单
|
|
263
|
+
// 是 useDirectoryOpenActions.ts 里的硬编码数组,不看这张表。
|
|
264
|
+
if (platform === 'win32') {
|
|
265
|
+
const wingetAvailable = hasCommand('winget', platform);
|
|
266
|
+
installers.gh = {
|
|
267
|
+
supported: wingetAvailable,
|
|
268
|
+
command: 'winget install --id GitHub.cli -e --accept-package-agreements --accept-source-agreements',
|
|
269
|
+
packageManager: 'winget',
|
|
270
|
+
docsUrl: TOOL_DOCS_URLS.gh,
|
|
271
|
+
note: wingetAvailable
|
|
272
|
+
? '将在新终端中通过 winget 安装 GitHub CLI;装完请在新终端执行 gh auth login 登录。'
|
|
273
|
+
: '未检测到 winget,请到 cli.github.com 下载安装包。',
|
|
274
|
+
executable: 'winget',
|
|
275
|
+
args: ['install', '--id', 'GitHub.cli', '-e', '--accept-package-agreements', '--accept-source-agreements'],
|
|
276
|
+
updateCommand: wingetAvailable
|
|
277
|
+
? 'winget upgrade --id GitHub.cli -e --accept-package-agreements --accept-source-agreements'
|
|
278
|
+
: undefined,
|
|
279
|
+
updatePackageManager: 'winget',
|
|
280
|
+
updateNote: wingetAvailable ? '将在新终端中通过 winget upgrade 升级 GitHub CLI。' : '未检测到 winget。',
|
|
281
|
+
updateExecutable: wingetAvailable ? 'winget' : undefined,
|
|
282
|
+
updateArgs: wingetAvailable
|
|
283
|
+
? ['upgrade', '--id', 'GitHub.cli', '-e', '--accept-package-agreements', '--accept-source-agreements']
|
|
284
|
+
: undefined,
|
|
285
|
+
};
|
|
286
|
+
} else if (platform === 'darwin') {
|
|
287
|
+
const brewAvailable = hasCommand('brew', platform);
|
|
288
|
+
installers.gh = {
|
|
289
|
+
supported: brewAvailable,
|
|
290
|
+
command: 'brew install gh',
|
|
291
|
+
packageManager: 'Homebrew',
|
|
292
|
+
docsUrl: TOOL_DOCS_URLS.gh,
|
|
293
|
+
note: brewAvailable
|
|
294
|
+
? '将在新终端中通过 Homebrew 安装 GitHub CLI;装完请执行 gh auth login 登录。'
|
|
295
|
+
: '未检测到 Homebrew,请到 cli.github.com 下载安装包。',
|
|
296
|
+
executable: 'brew',
|
|
297
|
+
args: ['install', 'gh'],
|
|
298
|
+
updateCommand: brewAvailable ? 'brew upgrade gh' : undefined,
|
|
299
|
+
updatePackageManager: 'Homebrew',
|
|
300
|
+
updateNote: brewAvailable ? '将在新终端中通过 brew upgrade 升级 GitHub CLI。' : '未检测到 brew。',
|
|
301
|
+
updateExecutable: brewAvailable ? 'brew' : undefined,
|
|
302
|
+
updateArgs: brewAvailable ? ['upgrade', 'gh'] : undefined,
|
|
303
|
+
};
|
|
304
|
+
} else {
|
|
305
|
+
// Debian/Ubuntu 官方源里已有 gh;其它发行版请看 docsUrl(Arch: pacman -S github-cli)
|
|
306
|
+
const aptAvailable = hasCommand('apt-get', platform);
|
|
307
|
+
installers.gh = {
|
|
308
|
+
supported: aptAvailable,
|
|
309
|
+
command: 'sudo apt-get install -y gh',
|
|
310
|
+
packageManager: 'apt',
|
|
311
|
+
docsUrl: TOOL_DOCS_URLS.gh,
|
|
312
|
+
note: aptAvailable
|
|
313
|
+
? '将在新终端中通过 apt 安装 GitHub CLI,可能需要输入系统密码;装完请执行 gh auth login 登录。'
|
|
314
|
+
: '当前 Linux 环境未检测到 apt-get,请按 docsUrl 里对应发行版的说明安装。',
|
|
315
|
+
executable: 'sudo',
|
|
316
|
+
args: ['apt-get', 'install', '-y', 'gh'],
|
|
317
|
+
// apt 没有"就地升级单个包"的稳定动词,不给一键更新,引导去官方文档
|
|
318
|
+
updateCommand: undefined,
|
|
319
|
+
updatePackageManager: 'apt',
|
|
320
|
+
updateNote: '请按官方文档升级 GitHub CLI。',
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
installers.gitee = {
|
|
324
|
+
supported: npmAvailable,
|
|
325
|
+
command: 'npm install -g @gitee/gitee-cli',
|
|
326
|
+
packageManager: 'npm',
|
|
327
|
+
docsUrl: TOOL_DOCS_URLS.gitee,
|
|
328
|
+
note: npmAvailable
|
|
329
|
+
? '将在新终端中通过 npm 全局安装 Gitee CLI;装完请执行 gitee auth login 粘贴私人令牌。'
|
|
330
|
+
: '未检测到 npm,请先安装 Node.js/npm,或按官方文档手动安装。',
|
|
331
|
+
executable: npmExecutable,
|
|
332
|
+
args: ['install', '-g', '@gitee/gitee-cli'],
|
|
333
|
+
updateCommand: 'npm install -g @gitee/gitee-cli@latest',
|
|
334
|
+
updatePackageManager: 'npm',
|
|
335
|
+
updateNote: '将在新终端中执行 `npm install -g @gitee/gitee-cli@latest`。已是最新时 npm 会跳过。',
|
|
336
|
+
updateExecutable: npmExecutable,
|
|
337
|
+
updateArgs: ['install', '-g', '@gitee/gitee-cli@latest'],
|
|
338
|
+
};
|
|
339
|
+
|
|
254
340
|
return installers;
|
|
255
341
|
}
|
|
256
342
|
|
|
257
|
-
function publicInstallerInfo(installers) {
|
|
343
|
+
export function publicInstallerInfo(installers) {
|
|
258
344
|
return Object.fromEntries(Object.entries(installers).map(([tool, installer]) => [tool, {
|
|
259
345
|
supported: installer.supported,
|
|
260
346
|
command: installer.command,
|
|
@@ -393,7 +479,13 @@ export async function launchGai(dirPath, {
|
|
|
393
479
|
'-WorkingDirectory (Get-Location).ProviderPath -WindowStyle Normal',
|
|
394
480
|
].join(' ');
|
|
395
481
|
return spawnFn('powershell.exe', ['-NoLogo', '-NoProfile', '-NonInteractive', '-EncodedCommand', encode(launcher)], {
|
|
396
|
-
cwd: dirPath,
|
|
482
|
+
cwd: dirPath,
|
|
483
|
+
windowsHide: true,
|
|
484
|
+
timeout: 10000,
|
|
485
|
+
// 这个终端窗口以及里面跑起来的 g ai,会继承下面这份 env;把注册表里新装的 CLI
|
|
486
|
+
// 目录补进去,否则窗口里 `gh repo list` 照样报"不是内部或外部命令"
|
|
487
|
+
// (本进程的 PATH 是启动快照)。见 utils/shellPath.js 顶部。
|
|
488
|
+
env: await augmentEnvPath(process.env),
|
|
397
489
|
});
|
|
398
490
|
}
|
|
399
491
|
|
|
@@ -577,6 +669,54 @@ async function launchToolInstaller(installer, dirPath = process.cwd(), { update
|
|
|
577
669
|
return launchInTerminal(dirPath, installer[executableField], installer[argsField]);
|
|
578
670
|
}
|
|
579
671
|
|
|
672
|
+
/**
|
|
673
|
+
* 在新终端窗口里跑一条**服务端白名单内的**命令,跑完窗口不自动关。
|
|
674
|
+
*
|
|
675
|
+
* 与 launchToolInstaller 的区别:那个吃 installer 描述符(executable + args),
|
|
676
|
+
* 这个直接吃整条命令串。给「登录」这类**交互式** CLI 用 —— `gh auth login`
|
|
677
|
+
* 会走一串问答(选 GitHub.com / 协议 / 浏览器还是粘贴 token),必须在真终端里跑:
|
|
678
|
+
* 被 spawn 出去又没有 TTY 的话,它直接就失败了,用户屏幕上什么也看不到。
|
|
679
|
+
*
|
|
680
|
+
* Windows 用 `cmd /k`:登录完成后窗口留着,用户能看清结果;失败也能看到报错。
|
|
681
|
+
*
|
|
682
|
+
* ⚠️ command 必须是服务端自己拼出来的常量。永远不要把它接成请求体里的字符串 ——
|
|
683
|
+
* 那等于开了一个任意命令执行的口子(和 /api/install-tool 同一条原则)。
|
|
684
|
+
*
|
|
685
|
+
* `pathDirs` 是**必须**看的参数(2026-09-22 实测踩到):
|
|
686
|
+
* 本进程的 `process.env.PATH` 是"启动那一刻"的快照。用户在界面里刚装好 gh,
|
|
687
|
+
* 安装器改的是**注册表**里的 PATH,我们这个已经跑着的 node 进程看不见 ——
|
|
688
|
+
* 于是出现很割裂的一幕:**探测**能找到 gh(它走注册表 + 默认落点),界面显示
|
|
689
|
+
* "gh 2.101.0 已安装",可 `cmd /k gh auth login` 这个子进程按自己的 PATH 找,
|
|
690
|
+
* 回一句 `'gh' 不是内部或外部命令`;而用户自己新开一个 cmd 却是好的,
|
|
691
|
+
* 因为新进程拿到的是最新 PATH。
|
|
692
|
+
* 解法有两层,缺一不可:
|
|
693
|
+
* 1. **基线兜底**(`augmentEnvPath`):把注册表 Machine + User 的 PATH 里本进程
|
|
694
|
+
* 快照没有的目录补进子进程 —— 语义上等价于"这个进程是现在才启动的"。
|
|
695
|
+
* 所以**不传 `pathDirs` 也不会踩旧快照**,`launchGai` 这类入口不用各自记得传。
|
|
696
|
+
* 2. `pathDirs`:调用方**已经知道 CLI 落点**时显式**前置**,抢在别的同名命令之前。
|
|
697
|
+
* 注意两层语义不同:兜底是追加(不动原有优先级),pathDirs 是前置(要优先)。
|
|
698
|
+
* 比起"把可执行文件全路径拼进命令行",前置 PATH 还顺带绕开了 cmd.exe 那套
|
|
699
|
+
* "路径带空格时的嵌套引号"地狱(`C:\Program Files\GitHub CLI\gh.exe`)。
|
|
700
|
+
*/
|
|
701
|
+
export async function launchCommandInTerminal(command, { cwd = process.cwd(), pathDirs = [] } = {}) {
|
|
702
|
+
const dirs = pathDirs.filter(Boolean);
|
|
703
|
+
const augmented = await augmentEnvPath(process.env);
|
|
704
|
+
let env;
|
|
705
|
+
if (dirs.length) {
|
|
706
|
+
env = withPathValue(augmented, [...dirs, pathValueOf(augmented)].filter(Boolean).join(path.delimiter));
|
|
707
|
+
} else {
|
|
708
|
+
// 没有要补的就传 undefined,让子进程原样继承父环境(与改动前行为一致)
|
|
709
|
+
env = augmented === process.env ? undefined : augmented;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if (process.platform === 'win32') {
|
|
713
|
+
return spawnDetached('cmd.exe', ['/c', 'start', '', 'cmd.exe', '/k', command], { cwd, env });
|
|
714
|
+
}
|
|
715
|
+
// posix 侧不额外处理 PATH:那边没有"安装器改注册表"这套机制,
|
|
716
|
+
// 终端模拟器直接继承图形会话的环境,不会出现上面的错配。
|
|
717
|
+
return launchInTerminal(cwd, 'bash', ['-lc', command]);
|
|
718
|
+
}
|
|
719
|
+
|
|
580
720
|
export function registerFileOpenRoutes({
|
|
581
721
|
app,
|
|
582
722
|
launchAi = launchGai,
|