diff --git a/src/main/services/team/GitDiffFallback.ts b/src/main/services/team/GitDiffFallback.ts index 1843da0c..1597725b 100644 --- a/src/main/services/team/GitDiffFallback.ts +++ b/src/main/services/team/GitDiffFallback.ts @@ -54,6 +54,7 @@ export class GitDiffFallback { cwd: projectPath, maxBuffer: GIT_MAX_BUFFER, timeout: GIT_TIMEOUT, + windowsHide: true, }); return stdout; } catch { @@ -75,7 +76,7 @@ export class GitDiffFallback { const { stdout } = await execFileAsync( 'git', ['log', '--format=%H', '--before', timestamp, '-1', '--', relativePath], - { cwd: projectPath, timeout: GIT_TIMEOUT } + { cwd: projectPath, timeout: GIT_TIMEOUT, windowsHide: true } ); return stdout.trim() || null; } catch { @@ -98,7 +99,7 @@ export class GitDiffFallback { const { stdout } = await execFileAsync( 'git', ['diff', fromCommit, toCommit, '--', relativePath], - { cwd: projectPath, timeout: GIT_TIMEOUT } + { cwd: projectPath, timeout: GIT_TIMEOUT, windowsHide: true } ); return stdout || null; } catch { @@ -120,7 +121,7 @@ export class GitDiffFallback { const { stdout } = await execFileAsync( 'git', ['log', `--max-count=${maxCount}`, '--format=%H|%aI|%s', '--', relativePath], - { cwd: projectPath, timeout: GIT_TIMEOUT } + { cwd: projectPath, timeout: GIT_TIMEOUT, windowsHide: true } ); return stdout @@ -148,6 +149,7 @@ export class GitDiffFallback { await execFileAsync('git', ['rev-parse', '--is-inside-work-tree'], { cwd: projectPath, timeout: GIT_TIMEOUT, + windowsHide: true, }); this.gitRepoCache.set(projectPath, true); return true; diff --git a/src/main/services/team/TeamMcpConfigBuilder.ts b/src/main/services/team/TeamMcpConfigBuilder.ts index d34e856a..2411b3ea 100644 --- a/src/main/services/team/TeamMcpConfigBuilder.ts +++ b/src/main/services/team/TeamMcpConfigBuilder.ts @@ -3,8 +3,8 @@ import { getMcpConfigsBasePath, getMcpServerBasePath, } from '@main/utils/pathDecoder'; +import { execCli } from '@main/utils/childProcess'; import { createLogger } from '@shared/utils/logger'; -import { execFile } from 'child_process'; import { randomUUID } from 'crypto'; import * as fs from 'fs'; import * as path from 'path'; @@ -185,17 +185,11 @@ async function resolveNodePath(options?: McpLaunchSpecResolveOptions): Promise((resolve, reject) => { - execFile( - 'node', - ['-e', 'process.stdout.write(process.execPath)'], - { - encoding: 'utf-8', - timeout: 5000, - }, - (err, stdout) => (err ? reject(err) : resolve(stdout.trim())) - ); + const { stdout } = await execCli('node', ['-e', 'process.stdout.write(process.execPath)'], { + encoding: 'utf-8', + timeout: 5000, }); + const resolved = stdout.trim(); if (resolved) { _resolvedNodePath = resolved; emitProgress(options, 'node-runtime-found', 'Using resolved Node.js runtime...'); diff --git a/src/main/services/team/TeamMemberWorktreeManager.ts b/src/main/services/team/TeamMemberWorktreeManager.ts index 94b613e0..c4e941c0 100644 --- a/src/main/services/team/TeamMemberWorktreeManager.ts +++ b/src/main/services/team/TeamMemberWorktreeManager.ts @@ -28,7 +28,7 @@ function execGit(args: string[], cwd: string): Promise { execFile( 'git', args, - { cwd, timeout: GIT_TIMEOUT_MS, maxBuffer: 1024 * 1024 }, + { cwd, timeout: GIT_TIMEOUT_MS, maxBuffer: 1024 * 1024, windowsHide: true }, (error, stdout, stderr) => { if (error) { const message = String(stderr || error.message || 'git command failed').trim(); diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index dac4d51a..51c5dd4e 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -6426,6 +6426,7 @@ export class TeamProvisioningService { encoding: 'utf8', maxBuffer: 16 * 1024, timeout: 1000, + windowsHide: true, }, (error, stdout) => { if (error) { diff --git a/src/main/services/team/TeamWorktreeGitService.ts b/src/main/services/team/TeamWorktreeGitService.ts index f03e2c85..24dd6207 100644 --- a/src/main/services/team/TeamWorktreeGitService.ts +++ b/src/main/services/team/TeamWorktreeGitService.ts @@ -11,7 +11,7 @@ function execGit(args: string[], cwd: string): Promise { execFile( 'git', args, - { cwd, timeout: GIT_TIMEOUT_MS, maxBuffer: 1024 * 1024 }, + { cwd, timeout: GIT_TIMEOUT_MS, maxBuffer: 1024 * 1024, windowsHide: true }, (error, stdout, stderr) => { if (error) { const message = String(stderr || error.message || 'git command failed').trim(); diff --git a/src/main/services/team/opencode/bridge/OpenCodeManagedHostProcessCleanup.ts b/src/main/services/team/opencode/bridge/OpenCodeManagedHostProcessCleanup.ts index e63a2122..cf06b3f0 100644 --- a/src/main/services/team/opencode/bridge/OpenCodeManagedHostProcessCleanup.ts +++ b/src/main/services/team/opencode/bridge/OpenCodeManagedHostProcessCleanup.ts @@ -278,6 +278,7 @@ function execFileText( encoding: 'utf8', timeout, maxBuffer, + windowsHide: true, }, (error: ExecFileException | null, stdout: string | Buffer) => { if (error) { diff --git a/src/main/utils/childProcess.ts b/src/main/utils/childProcess.ts index 7305f824..619d384d 100644 --- a/src/main/utils/childProcess.ts +++ b/src/main/utils/childProcess.ts @@ -376,8 +376,8 @@ export function killProcessTree( 'System32', 'taskkill.exe' ); - execFile(taskkillPath, ['/T', '/F', '/PID', String(child.pid)], () => { - // Best-effort — ignore errors (process may have already exited) + execFile(taskkillPath, ['/T', '/F', '/PID', String(child.pid)], { windowsHide: true }, () => { + // Best-effort - ignore errors (process may have already exited) }); return; } catch { diff --git a/src/main/utils/processKill.ts b/src/main/utils/processKill.ts index fdcf4fcd..c02401ac 100644 --- a/src/main/utils/processKill.ts +++ b/src/main/utils/processKill.ts @@ -21,8 +21,8 @@ export function killProcessByPid(pid: number): void { 'System32', 'taskkill.exe' ); - execFile(taskkillPath, ['/T', '/F', '/PID', String(pid)], () => { - // Best-effort — ignore errors (process may have already exited) + execFile(taskkillPath, ['/T', '/F', '/PID', String(pid)], { windowsHide: true }, () => { + // Best-effort - ignore errors (process may have already exited) }); } catch { // taskkill failed to spawn, fall through to process.kill() diff --git a/src/main/utils/shellEnv.ts b/src/main/utils/shellEnv.ts index 5a621e72..f3cd0646 100644 --- a/src/main/utils/shellEnv.ts +++ b/src/main/utils/shellEnv.ts @@ -87,6 +87,7 @@ async function readShellEnv(shellPath: string, args: string[]): Promise ({ isPackaged: false, version: '9.9.9-test', }, - execFileMock: vi.fn( - ( - _file: string, - _args: readonly string[], - _options: - | { encoding?: string; timeout?: number } - | ((error: Error | null, stdout: string, stderr: string) => void), - callback?: (error: Error | null, stdout: string, stderr: string) => void - ) => { - const cb = typeof _options === 'function' ? _options : callback; - cb?.(null, '/mock/node', ''); - } - ), + execCliMock: vi.fn(async () => ({ stdout: '/mock/node', stderr: '' })), })); let mockHomeDir = ''; @@ -29,11 +17,11 @@ type ModuleLoad = (request: string, parent: NodeModule | undefined, isMain: bool const moduleInternal = Module as unknown as { _load: ModuleLoad }; const originalModuleLoad = moduleInternal._load; -vi.mock('child_process', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('@main/utils/childProcess', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, - execFile: hoisted.execFileMock, + execCli: hoisted.execCliMock, }; }); @@ -189,7 +177,7 @@ describe('TeamMcpConfigBuilder', () => { setAppDataBasePath(tempAppData); setPackagedMode(false); setResourcesPath(undefined); - hoisted.execFileMock.mockClear(); + hoisted.execCliMock.mockClear(); }); afterEach(() => { @@ -283,6 +271,21 @@ describe('TeamMcpConfigBuilder', () => { expectNodeEntry(server, builtEntry); }); + it('uses the shared CLI helper for the Node.js runtime resolver', async () => { + mockBuiltWorkspaceEntryAvailable(); + const builder = new TeamMcpConfigBuilder(); + + const configPath = await builder.writeConfigFile(); + createdPaths.push(configPath); + + expect(readGeneratedServer(configPath)?.command).toBe('/mock/node'); + expect(hoisted.execCliMock).toHaveBeenCalledWith( + 'node', + ['-e', 'process.stdout.write(process.execPath)'], + expect.objectContaining({ encoding: 'utf-8', timeout: 5000 }) + ); + }); + it('keeps generated team MCP config minimal and does not inline top-level user MCP', async () => { const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'team-mcp-home-')); const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'team-mcp-project-')); diff --git a/test/main/utils/shellEnv.test.ts b/test/main/utils/shellEnv.test.ts index 35aa053c..aa26b08f 100644 --- a/test/main/utils/shellEnv.test.ts +++ b/test/main/utils/shellEnv.test.ts @@ -117,13 +117,13 @@ describe('shellEnv', () => { 1, '/bin/zsh', ['-lic', 'env -0'], - expect.any(Object) + expect.objectContaining({ windowsHide: true }) ); expect(hoisted.spawn).toHaveBeenNthCalledWith( 2, '/bin/zsh', ['-ic', 'env -0'], - expect.any(Object) + expect.objectContaining({ windowsHide: true }) ); });