@google/gemini-cli-core 0.1.22 → 0.2.0-preview.0
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/dist/src/code_assist/oauth2.d.ts +1 -0
- package/dist/src/code_assist/oauth2.js +7 -2
- package/dist/src/code_assist/oauth2.js.map +1 -1
- package/dist/src/code_assist/oauth2.test.js +49 -1
- package/dist/src/code_assist/oauth2.test.js.map +1 -1
- package/dist/src/config/config.d.ts +19 -2
- package/dist/src/config/config.js +29 -4
- package/dist/src/config/config.js.map +1 -1
- package/dist/src/core/client.js +5 -2
- package/dist/src/core/client.js.map +1 -1
- package/dist/src/core/client.test.js +2 -1
- package/dist/src/core/client.test.js.map +1 -1
- package/dist/src/core/coreToolScheduler.d.ts +2 -1
- package/dist/src/core/coreToolScheduler.js +20 -2
- package/dist/src/core/coreToolScheduler.js.map +1 -1
- package/dist/src/core/coreToolScheduler.test.js +190 -12
- package/dist/src/core/coreToolScheduler.test.js.map +1 -1
- package/dist/src/core/geminiChat.js +1 -1
- package/dist/src/core/geminiChat.js.map +1 -1
- package/dist/src/core/loggingContentGenerator.d.ts +1 -5
- package/dist/src/core/loggingContentGenerator.js +4 -6
- package/dist/src/core/loggingContentGenerator.js.map +1 -1
- package/dist/src/core/nonInteractiveToolExecutor.d.ts +2 -2
- package/dist/src/core/nonInteractiveToolExecutor.js +10 -2
- package/dist/src/core/nonInteractiveToolExecutor.js.map +1 -1
- package/dist/src/core/nonInteractiveToolExecutor.test.js +17 -11
- package/dist/src/core/nonInteractiveToolExecutor.test.js.map +1 -1
- package/dist/src/core/subagent.js +5 -5
- package/dist/src/core/subagent.js.map +1 -1
- package/dist/src/core/subagent.test.js +3 -3
- package/dist/src/core/subagent.test.js.map +1 -1
- package/dist/src/generated/git-commit.d.ts +2 -2
- package/dist/src/generated/git-commit.js +2 -2
- package/dist/src/generated/git-commit.js.map +1 -1
- package/dist/src/ide/ide-client.d.ts +4 -0
- package/dist/src/ide/ide-client.js +36 -2
- package/dist/src/ide/ide-client.js.map +1 -1
- package/dist/src/ide/ide-installer.js +17 -4
- package/dist/src/ide/ide-installer.js.map +1 -1
- package/dist/src/ide/ide-installer.test.js +6 -4
- package/dist/src/ide/ide-installer.test.js.map +1 -1
- package/dist/src/ide/process-utils.d.ts +10 -5
- package/dist/src/ide/process-utils.js +113 -30
- package/dist/src/ide/process-utils.js.map +1 -1
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +3 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/services/chatRecordingService.d.ts +150 -0
- package/dist/src/services/chatRecordingService.js +318 -0
- package/dist/src/services/chatRecordingService.js.map +1 -0
- package/dist/src/services/chatRecordingService.test.d.ts +6 -0
- package/dist/src/services/chatRecordingService.test.js +288 -0
- package/dist/src/services/chatRecordingService.test.js.map +1 -0
- package/dist/src/services/fileSystemService.d.ts +31 -0
- package/dist/src/services/fileSystemService.js +18 -0
- package/dist/src/services/fileSystemService.js.map +1 -0
- package/dist/src/services/fileSystemService.test.d.ts +6 -0
- package/dist/src/services/fileSystemService.test.js +41 -0
- package/dist/src/services/fileSystemService.test.js.map +1 -0
- package/dist/src/services/shellExecutionService.d.ts +8 -10
- package/dist/src/services/shellExecutionService.js +289 -133
- package/dist/src/services/shellExecutionService.js.map +1 -1
- package/dist/src/services/shellExecutionService.test.js +275 -41
- package/dist/src/services/shellExecutionService.test.js.map +1 -1
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.d.ts +10 -3
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.js +57 -103
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.js.map +1 -1
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js +13 -4
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js.map +1 -1
- package/dist/src/telemetry/clearcut-logger/event-metadata-key.d.ts +2 -1
- package/dist/src/telemetry/clearcut-logger/event-metadata-key.js +2 -0
- package/dist/src/telemetry/clearcut-logger/event-metadata-key.js.map +1 -1
- package/dist/src/telemetry/loggers.js +2 -2
- package/dist/src/telemetry/loggers.js.map +1 -1
- package/dist/src/telemetry/loggers.test.js +10 -5
- package/dist/src/telemetry/loggers.test.js.map +1 -1
- package/dist/src/telemetry/metrics.d.ts +1 -1
- package/dist/src/telemetry/metrics.js +2 -1
- package/dist/src/telemetry/metrics.js.map +1 -1
- package/dist/src/telemetry/sdk.js +3 -3
- package/dist/src/telemetry/sdk.js.map +1 -1
- package/dist/src/telemetry/types.d.ts +1 -0
- package/dist/src/telemetry/types.js +6 -0
- package/dist/src/telemetry/types.js.map +1 -1
- package/dist/src/telemetry/uiTelemetry.d.ts +4 -0
- package/dist/src/telemetry/uiTelemetry.js +14 -1
- package/dist/src/telemetry/uiTelemetry.js.map +1 -1
- package/dist/src/telemetry/uiTelemetry.test.js +45 -9
- package/dist/src/telemetry/uiTelemetry.test.js.map +1 -1
- package/dist/src/test-utils/tools.d.ts +8 -2
- package/dist/src/test-utils/tools.js +2 -3
- package/dist/src/test-utils/tools.js.map +1 -1
- package/dist/src/tools/edit.d.ts +1 -1
- package/dist/src/tools/edit.js +15 -9
- package/dist/src/tools/edit.js.map +1 -1
- package/dist/src/tools/edit.test.js +11 -1
- package/dist/src/tools/edit.test.js.map +1 -1
- package/dist/src/tools/glob.d.ts +1 -1
- package/dist/src/tools/glob.js +8 -8
- package/dist/src/tools/glob.js.map +1 -1
- package/dist/src/tools/glob.test.js +20 -0
- package/dist/src/tools/glob.test.js.map +1 -1
- package/dist/src/tools/grep.d.ts +1 -1
- package/dist/src/tools/grep.js +1 -6
- package/dist/src/tools/grep.js.map +1 -1
- package/dist/src/tools/ls.d.ts +1 -1
- package/dist/src/tools/ls.js +1 -6
- package/dist/src/tools/ls.js.map +1 -1
- package/dist/src/tools/mcp-client-manager.d.ts +38 -0
- package/dist/src/tools/mcp-client-manager.js +74 -0
- package/dist/src/tools/mcp-client-manager.js.map +1 -0
- package/dist/src/tools/mcp-client-manager.test.d.ts +6 -0
- package/dist/src/tools/mcp-client-manager.test.js +39 -0
- package/dist/src/tools/mcp-client-manager.test.js.map +1 -0
- package/dist/src/tools/mcp-client.d.ts +43 -0
- package/dist/src/tools/mcp-client.js +117 -1
- package/dist/src/tools/mcp-client.js.map +1 -1
- package/dist/src/tools/mcp-client.test.js +58 -302
- package/dist/src/tools/mcp-client.test.js.map +1 -1
- package/dist/src/tools/mcp-tool.test.js +17 -17
- package/dist/src/tools/mcp-tool.test.js.map +1 -1
- package/dist/src/tools/memoryTool.d.ts +1 -1
- package/dist/src/tools/memoryTool.js +1 -6
- package/dist/src/tools/memoryTool.js.map +1 -1
- package/dist/src/tools/read-file.d.ts +1 -1
- package/dist/src/tools/read-file.js +5 -7
- package/dist/src/tools/read-file.js.map +1 -1
- package/dist/src/tools/read-file.test.js +8 -0
- package/dist/src/tools/read-file.test.js.map +1 -1
- package/dist/src/tools/read-many-files.d.ts +0 -1
- package/dist/src/tools/read-many-files.js +16 -11
- package/dist/src/tools/read-many-files.js.map +1 -1
- package/dist/src/tools/read-many-files.test.js +34 -0
- package/dist/src/tools/read-many-files.test.js.map +1 -1
- package/dist/src/tools/shell.d.ts +1 -1
- package/dist/src/tools/shell.js +10 -24
- package/dist/src/tools/shell.js.map +1 -1
- package/dist/src/tools/shell.test.js +11 -19
- package/dist/src/tools/shell.test.js.map +1 -1
- package/dist/src/tools/tool-registry.d.ts +5 -0
- package/dist/src/tools/tool-registry.js +13 -4
- package/dist/src/tools/tool-registry.js.map +1 -1
- package/dist/src/tools/tool-registry.test.js +9 -21
- package/dist/src/tools/tool-registry.test.js.map +1 -1
- package/dist/src/tools/tools.d.ts +3 -1
- package/dist/src/tools/tools.js +12 -0
- package/dist/src/tools/tools.js.map +1 -1
- package/dist/src/tools/web-fetch.d.ts +1 -1
- package/dist/src/tools/web-fetch.js +1 -6
- package/dist/src/tools/web-fetch.js.map +1 -1
- package/dist/src/tools/web-search.d.ts +1 -1
- package/dist/src/tools/web-search.js +1 -6
- package/dist/src/tools/web-search.js.map +1 -1
- package/dist/src/tools/write-file.d.ts +1 -1
- package/dist/src/tools/write-file.js +7 -8
- package/dist/src/tools/write-file.js.map +1 -1
- package/dist/src/tools/write-file.test.js +25 -38
- package/dist/src/tools/write-file.test.js.map +1 -1
- package/dist/src/utils/environmentContext.js +2 -2
- package/dist/src/utils/environmentContext.js.map +1 -1
- package/dist/src/utils/environmentContext.test.js +3 -2
- package/dist/src/utils/environmentContext.test.js.map +1 -1
- package/dist/src/utils/fileUtils.d.ts +2 -1
- package/dist/src/utils/fileUtils.js +3 -3
- package/dist/src/utils/fileUtils.js.map +1 -1
- package/dist/src/utils/fileUtils.test.js +18 -17
- package/dist/src/utils/fileUtils.test.js.map +1 -1
- package/dist/src/utils/getPty.d.ts +19 -0
- package/dist/src/utils/getPty.js +23 -0
- package/dist/src/utils/getPty.js.map +1 -0
- package/dist/src/utils/user_account.js +58 -48
- package/dist/src/utils/user_account.js.map +1 -1
- package/dist/src/utils/user_account.test.js +76 -9
- package/dist/src/utils/user_account.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +20 -10
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Interface for file system operations that may be delegated to different implementations
|
|
8
|
+
*/
|
|
9
|
+
export interface FileSystemService {
|
|
10
|
+
/**
|
|
11
|
+
* Read text content from a file
|
|
12
|
+
*
|
|
13
|
+
* @param filePath - The path to the file to read
|
|
14
|
+
* @returns The file content as a string
|
|
15
|
+
*/
|
|
16
|
+
readTextFile(filePath: string): Promise<string>;
|
|
17
|
+
/**
|
|
18
|
+
* Write text content to a file
|
|
19
|
+
*
|
|
20
|
+
* @param filePath - The path to the file to write
|
|
21
|
+
* @param content - The content to write
|
|
22
|
+
*/
|
|
23
|
+
writeTextFile(filePath: string, content: string): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Standard file system implementation
|
|
27
|
+
*/
|
|
28
|
+
export declare class StandardFileSystemService implements FileSystemService {
|
|
29
|
+
readTextFile(filePath: string): Promise<string>;
|
|
30
|
+
writeTextFile(filePath: string, content: string): Promise<void>;
|
|
31
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
/**
|
|
8
|
+
* Standard file system implementation
|
|
9
|
+
*/
|
|
10
|
+
export class StandardFileSystemService {
|
|
11
|
+
async readTextFile(filePath) {
|
|
12
|
+
return fs.readFile(filePath, 'utf-8');
|
|
13
|
+
}
|
|
14
|
+
async writeTextFile(filePath, content) {
|
|
15
|
+
await fs.writeFile(filePath, content, 'utf-8');
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=fileSystemService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fileSystemService.js","sourceRoot":"","sources":["../../../src/services/fileSystemService.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,MAAM,aAAa,CAAC;AAuB7B;;GAEG;AACH,MAAM,OAAO,yBAAyB;IACpC,KAAK,CAAC,YAAY,CAAC,QAAgB;QACjC,OAAO,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IACxC,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,QAAgB,EAAE,OAAe;QACnD,MAAM,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;IACjD,CAAC;CACF"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
7
|
+
import fs from 'fs/promises';
|
|
8
|
+
import { StandardFileSystemService } from './fileSystemService.js';
|
|
9
|
+
vi.mock('fs/promises');
|
|
10
|
+
describe('StandardFileSystemService', () => {
|
|
11
|
+
let fileSystem;
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
vi.resetAllMocks();
|
|
14
|
+
fileSystem = new StandardFileSystemService();
|
|
15
|
+
});
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
vi.restoreAllMocks();
|
|
18
|
+
});
|
|
19
|
+
describe('readTextFile', () => {
|
|
20
|
+
it('should read file content using fs', async () => {
|
|
21
|
+
const testContent = 'Hello, World!';
|
|
22
|
+
vi.mocked(fs.readFile).mockResolvedValue(testContent);
|
|
23
|
+
const result = await fileSystem.readTextFile('/test/file.txt');
|
|
24
|
+
expect(fs.readFile).toHaveBeenCalledWith('/test/file.txt', 'utf-8');
|
|
25
|
+
expect(result).toBe(testContent);
|
|
26
|
+
});
|
|
27
|
+
it('should propagate fs.readFile errors', async () => {
|
|
28
|
+
const error = new Error('ENOENT: File not found');
|
|
29
|
+
vi.mocked(fs.readFile).mockRejectedValue(error);
|
|
30
|
+
await expect(fileSystem.readTextFile('/test/file.txt')).rejects.toThrow('ENOENT: File not found');
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
describe('writeTextFile', () => {
|
|
34
|
+
it('should write file content using fs', async () => {
|
|
35
|
+
vi.mocked(fs.writeFile).mockResolvedValue();
|
|
36
|
+
await fileSystem.writeTextFile('/test/file.txt', 'Hello, World!');
|
|
37
|
+
expect(fs.writeFile).toHaveBeenCalledWith('/test/file.txt', 'Hello, World!', 'utf-8');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
//# sourceMappingURL=fileSystemService.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fileSystemService.test.js","sourceRoot":"","sources":["../../../src/services/fileSystemService.test.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AACzE,OAAO,EAAE,MAAM,aAAa,CAAC;AAC7B,OAAO,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AAEnE,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;AAEvB,QAAQ,CAAC,2BAA2B,EAAE,GAAG,EAAE;IACzC,IAAI,UAAqC,CAAC;IAE1C,UAAU,CAAC,GAAG,EAAE;QACd,EAAE,CAAC,aAAa,EAAE,CAAC;QACnB,UAAU,GAAG,IAAI,yBAAyB,EAAE,CAAC;IAC/C,CAAC,CAAC,CAAC;IAEH,SAAS,CAAC,GAAG,EAAE;QACb,EAAE,CAAC,eAAe,EAAE,CAAC;IACvB,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,cAAc,EAAE,GAAG,EAAE;QAC5B,EAAE,CAAC,mCAAmC,EAAE,KAAK,IAAI,EAAE;YACjD,MAAM,WAAW,GAAG,eAAe,CAAC;YACpC,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,iBAAiB,CAAC,WAAW,CAAC,CAAC;YAEtD,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,YAAY,CAAC,gBAAgB,CAAC,CAAC;YAE/D,MAAM,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,oBAAoB,CAAC,gBAAgB,EAAE,OAAO,CAAC,CAAC;YACpE,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QACnC,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,qCAAqC,EAAE,KAAK,IAAI,EAAE;YACnD,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAC;YAClD,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC;YAEhD,MAAM,MAAM,CAAC,UAAU,CAAC,YAAY,CAAC,gBAAgB,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CACrE,wBAAwB,CACzB,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;QAC7B,EAAE,CAAC,oCAAoC,EAAE,KAAK,IAAI,EAAE;YAClD,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,iBAAiB,EAAE,CAAC;YAE5C,MAAM,UAAU,CAAC,aAAa,CAAC,gBAAgB,EAAE,eAAe,CAAC,CAAC;YAElE,MAAM,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,oBAAoB,CACvC,gBAAgB,EAChB,eAAe,EACf,OAAO,CACR,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -7,22 +7,20 @@
|
|
|
7
7
|
export interface ShellExecutionResult {
|
|
8
8
|
/** The raw, unprocessed output buffer. */
|
|
9
9
|
rawOutput: Buffer;
|
|
10
|
-
/** The combined, decoded
|
|
10
|
+
/** The combined, decoded output as a string. */
|
|
11
11
|
output: string;
|
|
12
|
-
/** The decoded stdout as a string. */
|
|
13
|
-
stdout: string;
|
|
14
|
-
/** The decoded stderr as a string. */
|
|
15
|
-
stderr: string;
|
|
16
12
|
/** The process exit code, or null if terminated by a signal. */
|
|
17
13
|
exitCode: number | null;
|
|
18
14
|
/** The signal that terminated the process, if any. */
|
|
19
|
-
signal:
|
|
15
|
+
signal: number | null;
|
|
20
16
|
/** An error object if the process failed to spawn. */
|
|
21
17
|
error: Error | null;
|
|
22
18
|
/** A boolean indicating if the command was aborted by the user. */
|
|
23
19
|
aborted: boolean;
|
|
24
20
|
/** The process ID of the spawned shell. */
|
|
25
21
|
pid: number | undefined;
|
|
22
|
+
/** The method used to execute the shell command. */
|
|
23
|
+
executionMethod: 'lydell-node-pty' | 'node-pty' | 'child_process' | 'none';
|
|
26
24
|
}
|
|
27
25
|
/** A handle for an ongoing shell execution. */
|
|
28
26
|
export interface ShellExecutionHandle {
|
|
@@ -37,8 +35,6 @@ export interface ShellExecutionHandle {
|
|
|
37
35
|
export type ShellOutputEvent = {
|
|
38
36
|
/** The event contains a chunk of output data. */
|
|
39
37
|
type: 'data';
|
|
40
|
-
/** The stream from which the data originated. */
|
|
41
|
-
stream: 'stdout' | 'stderr';
|
|
42
38
|
/** The decoded string chunk. */
|
|
43
39
|
chunk: string;
|
|
44
40
|
} | {
|
|
@@ -57,7 +53,7 @@ export type ShellOutputEvent = {
|
|
|
57
53
|
*/
|
|
58
54
|
export declare class ShellExecutionService {
|
|
59
55
|
/**
|
|
60
|
-
* Executes a shell command using `
|
|
56
|
+
* Executes a shell command using `node-pty`, capturing all output and lifecycle events.
|
|
61
57
|
*
|
|
62
58
|
* @param commandToExecute The exact command string to run.
|
|
63
59
|
* @param cwd The working directory to execute the command in.
|
|
@@ -66,5 +62,7 @@ export declare class ShellExecutionService {
|
|
|
66
62
|
* @returns An object containing the process ID (pid) and a promise that
|
|
67
63
|
* resolves with the complete execution result.
|
|
68
64
|
*/
|
|
69
|
-
static execute(commandToExecute: string, cwd: string, onOutputEvent: (event: ShellOutputEvent) => void, abortSignal: AbortSignal): ShellExecutionHandle
|
|
65
|
+
static execute(commandToExecute: string, cwd: string, onOutputEvent: (event: ShellOutputEvent) => void, abortSignal: AbortSignal, shouldUseNodePty: boolean, terminalColumns?: number, terminalRows?: number): Promise<ShellExecutionHandle>;
|
|
66
|
+
private static childProcessFallback;
|
|
67
|
+
private static executeWithPty;
|
|
70
68
|
}
|
|
@@ -3,13 +3,26 @@
|
|
|
3
3
|
* Copyright 2025 Google LLC
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import { getPty } from '../utils/getPty.js';
|
|
7
|
+
import { spawn as cpSpawn } from 'child_process';
|
|
7
8
|
import { TextDecoder } from 'util';
|
|
8
9
|
import os from 'os';
|
|
9
|
-
import stripAnsi from 'strip-ansi';
|
|
10
10
|
import { getCachedEncodingForBuffer } from '../utils/systemEncoding.js';
|
|
11
11
|
import { isBinary } from '../utils/textUtils.js';
|
|
12
|
+
import pkg from '@xterm/headless';
|
|
13
|
+
import stripAnsi from 'strip-ansi';
|
|
14
|
+
const { Terminal } = pkg;
|
|
12
15
|
const SIGKILL_TIMEOUT_MS = 200;
|
|
16
|
+
// @ts-expect-error getFullText is not a public API.
|
|
17
|
+
const getFullText = (terminal) => {
|
|
18
|
+
const buffer = terminal.buffer.active;
|
|
19
|
+
const lines = [];
|
|
20
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
21
|
+
const line = buffer.getLine(i);
|
|
22
|
+
lines.push(line ? line.translateToString(true) : '');
|
|
23
|
+
}
|
|
24
|
+
return lines.join('\n').trim();
|
|
25
|
+
};
|
|
13
26
|
/**
|
|
14
27
|
* A centralized service for executing shell commands with robust process
|
|
15
28
|
* management, cross-platform compatibility, and streaming output capabilities.
|
|
@@ -17,7 +30,7 @@ const SIGKILL_TIMEOUT_MS = 200;
|
|
|
17
30
|
*/
|
|
18
31
|
export class ShellExecutionService {
|
|
19
32
|
/**
|
|
20
|
-
* Executes a shell command using `
|
|
33
|
+
* Executes a shell command using `node-pty`, capturing all output and lifecycle events.
|
|
21
34
|
*
|
|
22
35
|
* @param commandToExecute The exact command string to run.
|
|
23
36
|
* @param cwd The working directory to execute the command in.
|
|
@@ -26,150 +39,293 @@ export class ShellExecutionService {
|
|
|
26
39
|
* @returns An object containing the process ID (pid) and a promise that
|
|
27
40
|
* resolves with the complete execution result.
|
|
28
41
|
*/
|
|
29
|
-
static execute(commandToExecute, cwd, onOutputEvent, abortSignal) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
42
|
+
static async execute(commandToExecute, cwd, onOutputEvent, abortSignal, shouldUseNodePty, terminalColumns, terminalRows) {
|
|
43
|
+
if (shouldUseNodePty) {
|
|
44
|
+
const ptyInfo = await getPty();
|
|
45
|
+
if (ptyInfo) {
|
|
46
|
+
try {
|
|
47
|
+
return this.executeWithPty(commandToExecute, cwd, onOutputEvent, abortSignal, terminalColumns, terminalRows, ptyInfo);
|
|
48
|
+
}
|
|
49
|
+
catch (_e) {
|
|
50
|
+
// Fallback to child_process
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return this.childProcessFallback(commandToExecute, cwd, onOutputEvent, abortSignal);
|
|
55
|
+
}
|
|
56
|
+
static childProcessFallback(commandToExecute, cwd, onOutputEvent, abortSignal) {
|
|
57
|
+
try {
|
|
58
|
+
const isWindows = os.platform() === 'win32';
|
|
59
|
+
const child = cpSpawn(commandToExecute, [], {
|
|
60
|
+
cwd,
|
|
61
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
62
|
+
shell: isWindows ? true : 'bash',
|
|
63
|
+
detached: !isWindows,
|
|
64
|
+
env: {
|
|
65
|
+
...process.env,
|
|
66
|
+
GEMINI_CLI: '1',
|
|
67
|
+
TERM: 'xterm-256color',
|
|
68
|
+
PAGER: 'cat',
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
const result = new Promise((resolve) => {
|
|
72
|
+
let stdoutDecoder = null;
|
|
73
|
+
let stderrDecoder = null;
|
|
74
|
+
let stdout = '';
|
|
75
|
+
let stderr = '';
|
|
76
|
+
const outputChunks = [];
|
|
77
|
+
let error = null;
|
|
78
|
+
let exited = false;
|
|
79
|
+
let isStreamingRawContent = true;
|
|
80
|
+
const MAX_SNIFF_SIZE = 4096;
|
|
81
|
+
let sniffedBytes = 0;
|
|
82
|
+
const handleOutput = (data, stream) => {
|
|
83
|
+
if (!stdoutDecoder || !stderrDecoder) {
|
|
84
|
+
const encoding = getCachedEncodingForBuffer(data);
|
|
85
|
+
try {
|
|
86
|
+
stdoutDecoder = new TextDecoder(encoding);
|
|
87
|
+
stderrDecoder = new TextDecoder(encoding);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
stdoutDecoder = new TextDecoder('utf-8');
|
|
91
|
+
stderrDecoder = new TextDecoder('utf-8');
|
|
92
|
+
}
|
|
63
93
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
94
|
+
outputChunks.push(data);
|
|
95
|
+
if (isStreamingRawContent && sniffedBytes < MAX_SNIFF_SIZE) {
|
|
96
|
+
const sniffBuffer = Buffer.concat(outputChunks.slice(0, 20));
|
|
97
|
+
sniffedBytes = sniffBuffer.length;
|
|
98
|
+
if (isBinary(sniffBuffer)) {
|
|
99
|
+
isStreamingRawContent = false;
|
|
100
|
+
onOutputEvent({ type: 'binary_detected' });
|
|
101
|
+
}
|
|
69
102
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
sniffedBytes = sniffBuffer.length;
|
|
76
|
-
if (isBinary(sniffBuffer)) {
|
|
77
|
-
// Change state to stop streaming raw content.
|
|
78
|
-
isStreamingRawContent = false;
|
|
79
|
-
onOutputEvent({ type: 'binary_detected' });
|
|
103
|
+
const decoder = stream === 'stdout' ? stdoutDecoder : stderrDecoder;
|
|
104
|
+
const decodedChunk = decoder.decode(data, { stream: true });
|
|
105
|
+
const strippedChunk = stripAnsi(decodedChunk);
|
|
106
|
+
if (stream === 'stdout') {
|
|
107
|
+
stdout += strippedChunk;
|
|
80
108
|
}
|
|
109
|
+
else {
|
|
110
|
+
stderr += strippedChunk;
|
|
111
|
+
}
|
|
112
|
+
if (isStreamingRawContent) {
|
|
113
|
+
onOutputEvent({ type: 'data', chunk: strippedChunk });
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
const totalBytes = outputChunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
117
|
+
onOutputEvent({
|
|
118
|
+
type: 'binary_progress',
|
|
119
|
+
bytesReceived: totalBytes,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
const handleExit = (code, signal) => {
|
|
124
|
+
const { finalBuffer } = cleanup();
|
|
125
|
+
// Ensure we don't add an extra newline if stdout already ends with one.
|
|
126
|
+
const separator = stdout.endsWith('\n') ? '' : '\n';
|
|
127
|
+
const combinedOutput = stdout + (stderr ? (stdout ? separator : '') + stderr : '');
|
|
128
|
+
resolve({
|
|
129
|
+
rawOutput: finalBuffer,
|
|
130
|
+
output: combinedOutput.trim(),
|
|
131
|
+
exitCode: code,
|
|
132
|
+
signal: signal ? os.constants.signals[signal] : null,
|
|
133
|
+
error,
|
|
134
|
+
aborted: abortSignal.aborted,
|
|
135
|
+
pid: child.pid,
|
|
136
|
+
executionMethod: 'child_process',
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
child.stdout.on('data', (data) => handleOutput(data, 'stdout'));
|
|
140
|
+
child.stderr.on('data', (data) => handleOutput(data, 'stderr'));
|
|
141
|
+
child.on('error', (err) => {
|
|
142
|
+
error = err;
|
|
143
|
+
handleExit(1, null);
|
|
144
|
+
});
|
|
145
|
+
const abortHandler = async () => {
|
|
146
|
+
if (child.pid && !exited) {
|
|
147
|
+
if (isWindows) {
|
|
148
|
+
cpSpawn('taskkill', ['/pid', child.pid.toString(), '/f', '/t']);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
try {
|
|
152
|
+
process.kill(-child.pid, 'SIGTERM');
|
|
153
|
+
await new Promise((res) => setTimeout(res, SIGKILL_TIMEOUT_MS));
|
|
154
|
+
if (!exited) {
|
|
155
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch (_e) {
|
|
159
|
+
if (!exited)
|
|
160
|
+
child.kill('SIGKILL');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
abortSignal.addEventListener('abort', abortHandler, { once: true });
|
|
166
|
+
child.on('exit', (code, signal) => {
|
|
167
|
+
handleExit(code, signal);
|
|
168
|
+
});
|
|
169
|
+
function cleanup() {
|
|
170
|
+
exited = true;
|
|
171
|
+
abortSignal.removeEventListener('abort', abortHandler);
|
|
172
|
+
if (stdoutDecoder) {
|
|
173
|
+
const remaining = stdoutDecoder.decode();
|
|
174
|
+
if (remaining) {
|
|
175
|
+
stdout += stripAnsi(remaining);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (stderrDecoder) {
|
|
179
|
+
const remaining = stderrDecoder.decode();
|
|
180
|
+
if (remaining) {
|
|
181
|
+
stderr += stripAnsi(remaining);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const finalBuffer = Buffer.concat(outputChunks);
|
|
185
|
+
return { stdout, stderr, finalBuffer };
|
|
81
186
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
stderr += strippedChunk;
|
|
91
|
-
}
|
|
92
|
-
if (isStreamingRawContent) {
|
|
93
|
-
onOutputEvent({ type: 'data', stream, chunk: strippedChunk });
|
|
94
|
-
}
|
|
95
|
-
else {
|
|
96
|
-
const totalBytes = outputChunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
97
|
-
onOutputEvent({ type: 'binary_progress', bytesReceived: totalBytes });
|
|
98
|
-
}
|
|
99
|
-
};
|
|
100
|
-
child.stdout.on('data', (data) => handleOutput(data, 'stdout'));
|
|
101
|
-
child.stderr.on('data', (data) => handleOutput(data, 'stderr'));
|
|
102
|
-
child.on('error', (err) => {
|
|
103
|
-
const { stdout, stderr, finalBuffer } = cleanup();
|
|
104
|
-
error = err;
|
|
105
|
-
resolve({
|
|
187
|
+
});
|
|
188
|
+
return { pid: child.pid, result };
|
|
189
|
+
}
|
|
190
|
+
catch (e) {
|
|
191
|
+
const error = e;
|
|
192
|
+
return {
|
|
193
|
+
pid: undefined,
|
|
194
|
+
result: Promise.resolve({
|
|
106
195
|
error,
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
rawOutput: finalBuffer,
|
|
110
|
-
output: stdout + (stderr ? `\n${stderr}` : ''),
|
|
196
|
+
rawOutput: Buffer.from(''),
|
|
197
|
+
output: '',
|
|
111
198
|
exitCode: 1,
|
|
112
199
|
signal: null,
|
|
113
200
|
aborted: false,
|
|
114
|
-
pid:
|
|
115
|
-
|
|
201
|
+
pid: undefined,
|
|
202
|
+
executionMethod: 'none',
|
|
203
|
+
}),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
static executeWithPty(commandToExecute, cwd, onOutputEvent, abortSignal, terminalColumns, terminalRows, ptyInfo) {
|
|
208
|
+
try {
|
|
209
|
+
const cols = terminalColumns ?? 80;
|
|
210
|
+
const rows = terminalRows ?? 30;
|
|
211
|
+
const isWindows = os.platform() === 'win32';
|
|
212
|
+
const shell = isWindows ? 'cmd.exe' : 'bash';
|
|
213
|
+
const args = isWindows
|
|
214
|
+
? ['/c', commandToExecute]
|
|
215
|
+
: ['-c', commandToExecute];
|
|
216
|
+
const ptyProcess = ptyInfo?.module.spawn(shell, args, {
|
|
217
|
+
cwd,
|
|
218
|
+
name: 'xterm-color',
|
|
219
|
+
cols,
|
|
220
|
+
rows,
|
|
221
|
+
env: {
|
|
222
|
+
...process.env,
|
|
223
|
+
GEMINI_CLI: '1',
|
|
224
|
+
TERM: 'xterm-256color',
|
|
225
|
+
PAGER: 'cat',
|
|
226
|
+
},
|
|
227
|
+
handleFlowControl: true,
|
|
116
228
|
});
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
229
|
+
const result = new Promise((resolve) => {
|
|
230
|
+
const headlessTerminal = new Terminal({
|
|
231
|
+
allowProposedApi: true,
|
|
232
|
+
cols,
|
|
233
|
+
rows,
|
|
234
|
+
});
|
|
235
|
+
let processingChain = Promise.resolve();
|
|
236
|
+
let decoder = null;
|
|
237
|
+
let output = '';
|
|
238
|
+
const outputChunks = [];
|
|
239
|
+
const error = null;
|
|
240
|
+
let exited = false;
|
|
241
|
+
let isStreamingRawContent = true;
|
|
242
|
+
const MAX_SNIFF_SIZE = 4096;
|
|
243
|
+
let sniffedBytes = 0;
|
|
244
|
+
const handleOutput = (data) => {
|
|
245
|
+
processingChain = processingChain.then(() => new Promise((resolve) => {
|
|
246
|
+
if (!decoder) {
|
|
247
|
+
const encoding = getCachedEncodingForBuffer(data);
|
|
248
|
+
try {
|
|
249
|
+
decoder = new TextDecoder(encoding);
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
decoder = new TextDecoder('utf-8');
|
|
130
253
|
}
|
|
131
254
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
255
|
+
outputChunks.push(data);
|
|
256
|
+
if (isStreamingRawContent && sniffedBytes < MAX_SNIFF_SIZE) {
|
|
257
|
+
const sniffBuffer = Buffer.concat(outputChunks.slice(0, 20));
|
|
258
|
+
sniffedBytes = sniffBuffer.length;
|
|
259
|
+
if (isBinary(sniffBuffer)) {
|
|
260
|
+
isStreamingRawContent = false;
|
|
261
|
+
onOutputEvent({ type: 'binary_detected' });
|
|
262
|
+
}
|
|
136
263
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
264
|
+
if (isStreamingRawContent) {
|
|
265
|
+
const decodedChunk = decoder.decode(data, { stream: true });
|
|
266
|
+
headlessTerminal.write(decodedChunk, () => {
|
|
267
|
+
const newStrippedOutput = getFullText(headlessTerminal);
|
|
268
|
+
output = newStrippedOutput;
|
|
269
|
+
onOutputEvent({ type: 'data', chunk: newStrippedOutput });
|
|
270
|
+
resolve();
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
const totalBytes = outputChunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
275
|
+
onOutputEvent({
|
|
276
|
+
type: 'binary_progress',
|
|
277
|
+
bytesReceived: totalBytes,
|
|
278
|
+
});
|
|
279
|
+
resolve();
|
|
280
|
+
}
|
|
281
|
+
}));
|
|
282
|
+
};
|
|
283
|
+
ptyProcess.onData((data) => {
|
|
284
|
+
const bufferData = Buffer.from(data, 'utf-8');
|
|
285
|
+
handleOutput(bufferData);
|
|
286
|
+
});
|
|
287
|
+
ptyProcess.onExit(({ exitCode, signal }) => {
|
|
288
|
+
exited = true;
|
|
289
|
+
abortSignal.removeEventListener('abort', abortHandler);
|
|
290
|
+
processingChain.then(() => {
|
|
291
|
+
const finalBuffer = Buffer.concat(outputChunks);
|
|
292
|
+
resolve({
|
|
293
|
+
rawOutput: finalBuffer,
|
|
294
|
+
output,
|
|
295
|
+
exitCode,
|
|
296
|
+
signal: signal ?? null,
|
|
297
|
+
error,
|
|
298
|
+
aborted: abortSignal.aborted,
|
|
299
|
+
pid: ptyProcess.pid,
|
|
300
|
+
executionMethod: ptyInfo?.name ?? 'node-pty',
|
|
301
|
+
});
|
|
302
|
+
});
|
|
153
303
|
});
|
|
304
|
+
const abortHandler = async () => {
|
|
305
|
+
if (ptyProcess.pid && !exited) {
|
|
306
|
+
ptyProcess.kill('SIGHUP');
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
abortSignal.addEventListener('abort', abortHandler, { once: true });
|
|
154
310
|
});
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
311
|
+
return { pid: ptyProcess.pid, result };
|
|
312
|
+
}
|
|
313
|
+
catch (e) {
|
|
314
|
+
const error = e;
|
|
315
|
+
return {
|
|
316
|
+
pid: undefined,
|
|
317
|
+
result: Promise.resolve({
|
|
318
|
+
error,
|
|
319
|
+
rawOutput: Buffer.from(''),
|
|
320
|
+
output: '',
|
|
321
|
+
exitCode: 1,
|
|
322
|
+
signal: null,
|
|
323
|
+
aborted: false,
|
|
324
|
+
pid: undefined,
|
|
325
|
+
executionMethod: 'none',
|
|
326
|
+
}),
|
|
327
|
+
};
|
|
328
|
+
}
|
|
173
329
|
}
|
|
174
330
|
}
|
|
175
331
|
//# sourceMappingURL=shellExecutionService.js.map
|