@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.
Files changed (176) hide show
  1. package/dist/src/code_assist/oauth2.d.ts +1 -0
  2. package/dist/src/code_assist/oauth2.js +7 -2
  3. package/dist/src/code_assist/oauth2.js.map +1 -1
  4. package/dist/src/code_assist/oauth2.test.js +49 -1
  5. package/dist/src/code_assist/oauth2.test.js.map +1 -1
  6. package/dist/src/config/config.d.ts +19 -2
  7. package/dist/src/config/config.js +29 -4
  8. package/dist/src/config/config.js.map +1 -1
  9. package/dist/src/core/client.js +5 -2
  10. package/dist/src/core/client.js.map +1 -1
  11. package/dist/src/core/client.test.js +2 -1
  12. package/dist/src/core/client.test.js.map +1 -1
  13. package/dist/src/core/coreToolScheduler.d.ts +2 -1
  14. package/dist/src/core/coreToolScheduler.js +20 -2
  15. package/dist/src/core/coreToolScheduler.js.map +1 -1
  16. package/dist/src/core/coreToolScheduler.test.js +190 -12
  17. package/dist/src/core/coreToolScheduler.test.js.map +1 -1
  18. package/dist/src/core/geminiChat.js +1 -1
  19. package/dist/src/core/geminiChat.js.map +1 -1
  20. package/dist/src/core/loggingContentGenerator.d.ts +1 -5
  21. package/dist/src/core/loggingContentGenerator.js +4 -6
  22. package/dist/src/core/loggingContentGenerator.js.map +1 -1
  23. package/dist/src/core/nonInteractiveToolExecutor.d.ts +2 -2
  24. package/dist/src/core/nonInteractiveToolExecutor.js +10 -2
  25. package/dist/src/core/nonInteractiveToolExecutor.js.map +1 -1
  26. package/dist/src/core/nonInteractiveToolExecutor.test.js +17 -11
  27. package/dist/src/core/nonInteractiveToolExecutor.test.js.map +1 -1
  28. package/dist/src/core/subagent.js +5 -5
  29. package/dist/src/core/subagent.js.map +1 -1
  30. package/dist/src/core/subagent.test.js +3 -3
  31. package/dist/src/core/subagent.test.js.map +1 -1
  32. package/dist/src/generated/git-commit.d.ts +2 -2
  33. package/dist/src/generated/git-commit.js +2 -2
  34. package/dist/src/generated/git-commit.js.map +1 -1
  35. package/dist/src/ide/ide-client.d.ts +4 -0
  36. package/dist/src/ide/ide-client.js +36 -2
  37. package/dist/src/ide/ide-client.js.map +1 -1
  38. package/dist/src/ide/ide-installer.js +17 -4
  39. package/dist/src/ide/ide-installer.js.map +1 -1
  40. package/dist/src/ide/ide-installer.test.js +6 -4
  41. package/dist/src/ide/ide-installer.test.js.map +1 -1
  42. package/dist/src/ide/process-utils.d.ts +10 -5
  43. package/dist/src/ide/process-utils.js +113 -30
  44. package/dist/src/ide/process-utils.js.map +1 -1
  45. package/dist/src/index.d.ts +3 -0
  46. package/dist/src/index.js +3 -0
  47. package/dist/src/index.js.map +1 -1
  48. package/dist/src/services/chatRecordingService.d.ts +150 -0
  49. package/dist/src/services/chatRecordingService.js +318 -0
  50. package/dist/src/services/chatRecordingService.js.map +1 -0
  51. package/dist/src/services/chatRecordingService.test.d.ts +6 -0
  52. package/dist/src/services/chatRecordingService.test.js +288 -0
  53. package/dist/src/services/chatRecordingService.test.js.map +1 -0
  54. package/dist/src/services/fileSystemService.d.ts +31 -0
  55. package/dist/src/services/fileSystemService.js +18 -0
  56. package/dist/src/services/fileSystemService.js.map +1 -0
  57. package/dist/src/services/fileSystemService.test.d.ts +6 -0
  58. package/dist/src/services/fileSystemService.test.js +41 -0
  59. package/dist/src/services/fileSystemService.test.js.map +1 -0
  60. package/dist/src/services/shellExecutionService.d.ts +8 -10
  61. package/dist/src/services/shellExecutionService.js +289 -133
  62. package/dist/src/services/shellExecutionService.js.map +1 -1
  63. package/dist/src/services/shellExecutionService.test.js +275 -41
  64. package/dist/src/services/shellExecutionService.test.js.map +1 -1
  65. package/dist/src/telemetry/clearcut-logger/clearcut-logger.d.ts +10 -3
  66. package/dist/src/telemetry/clearcut-logger/clearcut-logger.js +57 -103
  67. package/dist/src/telemetry/clearcut-logger/clearcut-logger.js.map +1 -1
  68. package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js +13 -4
  69. package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js.map +1 -1
  70. package/dist/src/telemetry/clearcut-logger/event-metadata-key.d.ts +2 -1
  71. package/dist/src/telemetry/clearcut-logger/event-metadata-key.js +2 -0
  72. package/dist/src/telemetry/clearcut-logger/event-metadata-key.js.map +1 -1
  73. package/dist/src/telemetry/loggers.js +2 -2
  74. package/dist/src/telemetry/loggers.js.map +1 -1
  75. package/dist/src/telemetry/loggers.test.js +10 -5
  76. package/dist/src/telemetry/loggers.test.js.map +1 -1
  77. package/dist/src/telemetry/metrics.d.ts +1 -1
  78. package/dist/src/telemetry/metrics.js +2 -1
  79. package/dist/src/telemetry/metrics.js.map +1 -1
  80. package/dist/src/telemetry/sdk.js +3 -3
  81. package/dist/src/telemetry/sdk.js.map +1 -1
  82. package/dist/src/telemetry/types.d.ts +1 -0
  83. package/dist/src/telemetry/types.js +6 -0
  84. package/dist/src/telemetry/types.js.map +1 -1
  85. package/dist/src/telemetry/uiTelemetry.d.ts +4 -0
  86. package/dist/src/telemetry/uiTelemetry.js +14 -1
  87. package/dist/src/telemetry/uiTelemetry.js.map +1 -1
  88. package/dist/src/telemetry/uiTelemetry.test.js +45 -9
  89. package/dist/src/telemetry/uiTelemetry.test.js.map +1 -1
  90. package/dist/src/test-utils/tools.d.ts +8 -2
  91. package/dist/src/test-utils/tools.js +2 -3
  92. package/dist/src/test-utils/tools.js.map +1 -1
  93. package/dist/src/tools/edit.d.ts +1 -1
  94. package/dist/src/tools/edit.js +15 -9
  95. package/dist/src/tools/edit.js.map +1 -1
  96. package/dist/src/tools/edit.test.js +11 -1
  97. package/dist/src/tools/edit.test.js.map +1 -1
  98. package/dist/src/tools/glob.d.ts +1 -1
  99. package/dist/src/tools/glob.js +8 -8
  100. package/dist/src/tools/glob.js.map +1 -1
  101. package/dist/src/tools/glob.test.js +20 -0
  102. package/dist/src/tools/glob.test.js.map +1 -1
  103. package/dist/src/tools/grep.d.ts +1 -1
  104. package/dist/src/tools/grep.js +1 -6
  105. package/dist/src/tools/grep.js.map +1 -1
  106. package/dist/src/tools/ls.d.ts +1 -1
  107. package/dist/src/tools/ls.js +1 -6
  108. package/dist/src/tools/ls.js.map +1 -1
  109. package/dist/src/tools/mcp-client-manager.d.ts +38 -0
  110. package/dist/src/tools/mcp-client-manager.js +74 -0
  111. package/dist/src/tools/mcp-client-manager.js.map +1 -0
  112. package/dist/src/tools/mcp-client-manager.test.d.ts +6 -0
  113. package/dist/src/tools/mcp-client-manager.test.js +39 -0
  114. package/dist/src/tools/mcp-client-manager.test.js.map +1 -0
  115. package/dist/src/tools/mcp-client.d.ts +43 -0
  116. package/dist/src/tools/mcp-client.js +117 -1
  117. package/dist/src/tools/mcp-client.js.map +1 -1
  118. package/dist/src/tools/mcp-client.test.js +58 -302
  119. package/dist/src/tools/mcp-client.test.js.map +1 -1
  120. package/dist/src/tools/mcp-tool.test.js +17 -17
  121. package/dist/src/tools/mcp-tool.test.js.map +1 -1
  122. package/dist/src/tools/memoryTool.d.ts +1 -1
  123. package/dist/src/tools/memoryTool.js +1 -6
  124. package/dist/src/tools/memoryTool.js.map +1 -1
  125. package/dist/src/tools/read-file.d.ts +1 -1
  126. package/dist/src/tools/read-file.js +5 -7
  127. package/dist/src/tools/read-file.js.map +1 -1
  128. package/dist/src/tools/read-file.test.js +8 -0
  129. package/dist/src/tools/read-file.test.js.map +1 -1
  130. package/dist/src/tools/read-many-files.d.ts +0 -1
  131. package/dist/src/tools/read-many-files.js +16 -11
  132. package/dist/src/tools/read-many-files.js.map +1 -1
  133. package/dist/src/tools/read-many-files.test.js +34 -0
  134. package/dist/src/tools/read-many-files.test.js.map +1 -1
  135. package/dist/src/tools/shell.d.ts +1 -1
  136. package/dist/src/tools/shell.js +10 -24
  137. package/dist/src/tools/shell.js.map +1 -1
  138. package/dist/src/tools/shell.test.js +11 -19
  139. package/dist/src/tools/shell.test.js.map +1 -1
  140. package/dist/src/tools/tool-registry.d.ts +5 -0
  141. package/dist/src/tools/tool-registry.js +13 -4
  142. package/dist/src/tools/tool-registry.js.map +1 -1
  143. package/dist/src/tools/tool-registry.test.js +9 -21
  144. package/dist/src/tools/tool-registry.test.js.map +1 -1
  145. package/dist/src/tools/tools.d.ts +3 -1
  146. package/dist/src/tools/tools.js +12 -0
  147. package/dist/src/tools/tools.js.map +1 -1
  148. package/dist/src/tools/web-fetch.d.ts +1 -1
  149. package/dist/src/tools/web-fetch.js +1 -6
  150. package/dist/src/tools/web-fetch.js.map +1 -1
  151. package/dist/src/tools/web-search.d.ts +1 -1
  152. package/dist/src/tools/web-search.js +1 -6
  153. package/dist/src/tools/web-search.js.map +1 -1
  154. package/dist/src/tools/write-file.d.ts +1 -1
  155. package/dist/src/tools/write-file.js +7 -8
  156. package/dist/src/tools/write-file.js.map +1 -1
  157. package/dist/src/tools/write-file.test.js +25 -38
  158. package/dist/src/tools/write-file.test.js.map +1 -1
  159. package/dist/src/utils/environmentContext.js +2 -2
  160. package/dist/src/utils/environmentContext.js.map +1 -1
  161. package/dist/src/utils/environmentContext.test.js +3 -2
  162. package/dist/src/utils/environmentContext.test.js.map +1 -1
  163. package/dist/src/utils/fileUtils.d.ts +2 -1
  164. package/dist/src/utils/fileUtils.js +3 -3
  165. package/dist/src/utils/fileUtils.js.map +1 -1
  166. package/dist/src/utils/fileUtils.test.js +18 -17
  167. package/dist/src/utils/fileUtils.test.js.map +1 -1
  168. package/dist/src/utils/getPty.d.ts +19 -0
  169. package/dist/src/utils/getPty.js +23 -0
  170. package/dist/src/utils/getPty.js.map +1 -0
  171. package/dist/src/utils/user_account.js +58 -48
  172. package/dist/src/utils/user_account.js.map +1 -1
  173. package/dist/src/utils/user_account.test.js +76 -9
  174. package/dist/src/utils/user_account.test.js.map +1 -1
  175. package/dist/tsconfig.tsbuildinfo +1 -1
  176. 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,6 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ export {};
@@ -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 stdout and stderr as a string. */
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: NodeJS.Signals | null;
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 `spawn`, capturing all output and lifecycle events.
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 { spawn } from 'child_process';
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 `spawn`, capturing all output and lifecycle events.
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
- const isWindows = os.platform() === 'win32';
31
- const child = spawn(commandToExecute, [], {
32
- cwd,
33
- stdio: ['ignore', 'pipe', 'pipe'],
34
- // Use bash unless in Windows (since it doesn't support bash).
35
- // For windows, just use the default.
36
- shell: isWindows ? true : 'bash',
37
- // Use process groups on non-Windows for robust killing.
38
- // Windows process termination is handled by `taskkill /t`.
39
- detached: !isWindows,
40
- env: {
41
- ...process.env,
42
- GEMINI_CLI: '1',
43
- },
44
- });
45
- const result = new Promise((resolve) => {
46
- // Use decoders to handle multi-byte characters safely (for streaming output).
47
- let stdoutDecoder = null;
48
- let stderrDecoder = null;
49
- let stdout = '';
50
- let stderr = '';
51
- const outputChunks = [];
52
- let error = null;
53
- let exited = false;
54
- let isStreamingRawContent = true;
55
- const MAX_SNIFF_SIZE = 4096;
56
- let sniffedBytes = 0;
57
- const handleOutput = (data, stream) => {
58
- if (!stdoutDecoder || !stderrDecoder) {
59
- const encoding = getCachedEncodingForBuffer(data);
60
- try {
61
- stdoutDecoder = new TextDecoder(encoding);
62
- stderrDecoder = new TextDecoder(encoding);
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
- catch {
65
- // If the encoding is not supported, fall back to utf-8.
66
- // This can happen on some platforms for certain encodings like 'utf-32le'.
67
- stdoutDecoder = new TextDecoder('utf-8');
68
- stderrDecoder = new TextDecoder('utf-8');
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
- outputChunks.push(data);
72
- // Binary detection logic. This only runs until we've made a determination.
73
- if (isStreamingRawContent && sniffedBytes < MAX_SNIFF_SIZE) {
74
- const sniffBuffer = Buffer.concat(outputChunks.slice(0, 20));
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
- const decodedChunk = stream === 'stdout'
83
- ? stdoutDecoder.decode(data, { stream: true })
84
- : stderrDecoder.decode(data, { stream: true });
85
- const strippedChunk = stripAnsi(decodedChunk);
86
- if (stream === 'stdout') {
87
- stdout += strippedChunk;
88
- }
89
- else {
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
- stdout,
108
- stderr,
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: child.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 abortHandler = async () => {
118
- if (child.pid && !exited) {
119
- if (isWindows) {
120
- spawn('taskkill', ['/pid', child.pid.toString(), '/f', '/t']);
121
- }
122
- else {
123
- try {
124
- // Kill the entire process group (negative PID).
125
- // SIGTERM first, then SIGKILL if it doesn't die.
126
- process.kill(-child.pid, 'SIGTERM');
127
- await new Promise((res) => setTimeout(res, SIGKILL_TIMEOUT_MS));
128
- if (!exited) {
129
- process.kill(-child.pid, 'SIGKILL');
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
- catch (_e) {
133
- // Fall back to killing just the main process if group kill fails.
134
- if (!exited)
135
- child.kill('SIGKILL');
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
- abortSignal.addEventListener('abort', abortHandler, { once: true });
141
- child.on('exit', (code, signal) => {
142
- const { stdout, stderr, finalBuffer } = cleanup();
143
- resolve({
144
- rawOutput: finalBuffer,
145
- output: stdout + (stderr ? `\n${stderr}` : ''),
146
- stdout,
147
- stderr,
148
- exitCode: code,
149
- signal,
150
- error,
151
- aborted: abortSignal.aborted,
152
- pid: child.pid,
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
- * Cleans up a process (and it's accompanying state) that is exiting or
157
- * erroring and returns output formatted output buffers and strings
158
- */
159
- function cleanup() {
160
- exited = true;
161
- abortSignal.removeEventListener('abort', abortHandler);
162
- if (stdoutDecoder) {
163
- stdout += stripAnsi(stdoutDecoder.decode());
164
- }
165
- if (stderrDecoder) {
166
- stderr += stripAnsi(stderrDecoder.decode());
167
- }
168
- const finalBuffer = Buffer.concat(outputChunks);
169
- return { stdout, stderr, finalBuffer };
170
- }
171
- });
172
- return { pid: child.pid, result };
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