@cloudflare/sandbox 0.3.7 → 0.4.1

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 (120) hide show
  1. package/.turbo/turbo-build.log +44 -0
  2. package/CHANGELOG.md +6 -14
  3. package/Dockerfile +82 -18
  4. package/README.md +89 -824
  5. package/dist/{chunk-JTKON2SH.js → chunk-BCJ7SF3Q.js} +9 -5
  6. package/dist/chunk-BCJ7SF3Q.js.map +1 -0
  7. package/dist/chunk-BFVUNTP4.js +104 -0
  8. package/dist/chunk-BFVUNTP4.js.map +1 -0
  9. package/dist/{chunk-NNGBXDMY.js → chunk-EKSWCBCA.js} +3 -6
  10. package/dist/chunk-EKSWCBCA.js.map +1 -0
  11. package/dist/chunk-HGF554LH.js +2236 -0
  12. package/dist/chunk-HGF554LH.js.map +1 -0
  13. package/dist/{chunk-6UAWTJ5S.js → chunk-Z532A7QC.js} +13 -20
  14. package/dist/{chunk-6UAWTJ5S.js.map → chunk-Z532A7QC.js.map} +1 -1
  15. package/dist/file-stream.d.ts +16 -38
  16. package/dist/file-stream.js +1 -2
  17. package/dist/index.d.ts +6 -5
  18. package/dist/index.js +35 -39
  19. package/dist/index.js.map +1 -1
  20. package/dist/interpreter.d.ts +3 -3
  21. package/dist/interpreter.js +2 -2
  22. package/dist/request-handler.d.ts +4 -3
  23. package/dist/request-handler.js +4 -7
  24. package/dist/sandbox-D9K2ypln.d.ts +583 -0
  25. package/dist/sandbox.d.ts +3 -3
  26. package/dist/sandbox.js +4 -7
  27. package/dist/security.d.ts +4 -3
  28. package/dist/security.js +3 -3
  29. package/dist/sse-parser.js +1 -1
  30. package/package.json +11 -5
  31. package/src/clients/base-client.ts +280 -0
  32. package/src/clients/command-client.ts +115 -0
  33. package/src/clients/file-client.ts +269 -0
  34. package/src/clients/git-client.ts +92 -0
  35. package/src/clients/index.ts +63 -0
  36. package/src/{interpreter-client.ts → clients/interpreter-client.ts} +148 -171
  37. package/src/clients/port-client.ts +105 -0
  38. package/src/clients/process-client.ts +177 -0
  39. package/src/clients/sandbox-client.ts +41 -0
  40. package/src/clients/types.ts +84 -0
  41. package/src/clients/utility-client.ts +94 -0
  42. package/src/errors/adapter.ts +180 -0
  43. package/src/errors/classes.ts +469 -0
  44. package/src/errors/index.ts +105 -0
  45. package/src/file-stream.ts +119 -117
  46. package/src/index.ts +81 -69
  47. package/src/interpreter.ts +17 -8
  48. package/src/request-handler.ts +69 -43
  49. package/src/sandbox.ts +694 -533
  50. package/src/security.ts +14 -23
  51. package/src/sse-parser.ts +4 -8
  52. package/startup.sh +3 -0
  53. package/tests/base-client.test.ts +328 -0
  54. package/tests/command-client.test.ts +407 -0
  55. package/tests/file-client.test.ts +643 -0
  56. package/tests/file-stream.test.ts +306 -0
  57. package/tests/git-client.test.ts +328 -0
  58. package/tests/port-client.test.ts +301 -0
  59. package/tests/process-client.test.ts +658 -0
  60. package/tests/sandbox.test.ts +465 -0
  61. package/tests/sse-parser.test.ts +290 -0
  62. package/tests/utility-client.test.ts +266 -0
  63. package/tests/wrangler.jsonc +35 -0
  64. package/tsconfig.json +9 -1
  65. package/vitest.config.ts +31 -0
  66. package/container_src/bun.lock +0 -76
  67. package/container_src/circuit-breaker.ts +0 -121
  68. package/container_src/control-process.ts +0 -784
  69. package/container_src/handler/exec.ts +0 -185
  70. package/container_src/handler/file.ts +0 -457
  71. package/container_src/handler/git.ts +0 -130
  72. package/container_src/handler/ports.ts +0 -314
  73. package/container_src/handler/process.ts +0 -568
  74. package/container_src/handler/session.ts +0 -92
  75. package/container_src/index.ts +0 -601
  76. package/container_src/interpreter-service.ts +0 -276
  77. package/container_src/isolation.ts +0 -1213
  78. package/container_src/mime-processor.ts +0 -255
  79. package/container_src/package.json +0 -18
  80. package/container_src/runtime/executors/javascript/node_executor.ts +0 -123
  81. package/container_src/runtime/executors/python/ipython_executor.py +0 -338
  82. package/container_src/runtime/executors/typescript/ts_executor.ts +0 -138
  83. package/container_src/runtime/process-pool.ts +0 -464
  84. package/container_src/shell-escape.ts +0 -42
  85. package/container_src/startup.sh +0 -11
  86. package/container_src/types.ts +0 -131
  87. package/dist/chunk-32UDXUPC.js +0 -671
  88. package/dist/chunk-32UDXUPC.js.map +0 -1
  89. package/dist/chunk-5DILEXGY.js +0 -85
  90. package/dist/chunk-5DILEXGY.js.map +0 -1
  91. package/dist/chunk-D3U63BZP.js +0 -240
  92. package/dist/chunk-D3U63BZP.js.map +0 -1
  93. package/dist/chunk-FXYPFGOZ.js +0 -129
  94. package/dist/chunk-FXYPFGOZ.js.map +0 -1
  95. package/dist/chunk-JTKON2SH.js.map +0 -1
  96. package/dist/chunk-NNGBXDMY.js.map +0 -1
  97. package/dist/chunk-SQLJNZ3K.js +0 -674
  98. package/dist/chunk-SQLJNZ3K.js.map +0 -1
  99. package/dist/chunk-W7TVRPBG.js +0 -108
  100. package/dist/chunk-W7TVRPBG.js.map +0 -1
  101. package/dist/client-B3RUab0s.d.ts +0 -225
  102. package/dist/client.d.ts +0 -4
  103. package/dist/client.js +0 -7
  104. package/dist/client.js.map +0 -1
  105. package/dist/errors.d.ts +0 -95
  106. package/dist/errors.js +0 -27
  107. package/dist/errors.js.map +0 -1
  108. package/dist/interpreter-client.d.ts +0 -4
  109. package/dist/interpreter-client.js +0 -9
  110. package/dist/interpreter-client.js.map +0 -1
  111. package/dist/interpreter-types.d.ts +0 -259
  112. package/dist/interpreter-types.js +0 -9
  113. package/dist/interpreter-types.js.map +0 -1
  114. package/dist/types.d.ts +0 -453
  115. package/dist/types.js +0 -45
  116. package/dist/types.js.map +0 -1
  117. package/src/client.ts +0 -1048
  118. package/src/errors.ts +0 -219
  119. package/src/interpreter-types.ts +0 -390
  120. package/src/types.ts +0 -571
@@ -1,1213 +0,0 @@
1
- /**
2
- * Process Isolation
3
- *
4
- * Implements PID namespace isolation to secure the sandbox environment.
5
- * Executed commands run in isolated namespaces, preventing them from:
6
- * - Seeing or killing control plane processes (Bun)
7
- * - Accessing platform secrets in /proc
8
- * - Hijacking control plane ports
9
- *
10
- * ## Two-Process Architecture
11
- *
12
- * Parent Process (Node.js) → Control Process (Node.js) → Isolated Shell (Bash)
13
- *
14
- * The control process manages the isolated shell and handles all I/O through
15
- * temp files instead of stdout/stderr parsing. This approach handles:
16
- * - Binary data without corruption
17
- * - Large outputs without buffer issues
18
- * - Command output that might contain markers
19
- * - Clean recovery when shell dies
20
- *
21
- * ## Why file-based IPC?
22
- * Initial marker-based parsing (UUID markers in stdout) had too many edge cases.
23
- * File-based IPC reliably handles any output type.
24
- *
25
- * Requires CAP_SYS_ADMIN capability (available in production).
26
- * Falls back to regular execution in development.
27
- */
28
-
29
- import { type ChildProcess, spawn } from 'node:child_process';
30
- import { randomBytes, randomUUID } from 'node:crypto';
31
- import * as path from 'node:path';
32
- import type { ExecEvent, ExecResult } from '../src/types';
33
- import type { ProcessRecord, ProcessStatus } from './types';
34
-
35
- // Configuration constants
36
- const CONFIG = {
37
- // Timeouts (in milliseconds)
38
- READY_TIMEOUT_MS: 5000, // 5 seconds for control process to initialize
39
- SHUTDOWN_GRACE_PERIOD_MS: 500, // Grace period for cleanup on shutdown
40
- COMMAND_TIMEOUT_MS: parseInt(process.env.COMMAND_TIMEOUT_MS || '30000'), // 30 seconds for command execution
41
- CLEANUP_INTERVAL_MS: parseInt(process.env.CLEANUP_INTERVAL_MS || '30000'), // Run cleanup every 30 seconds
42
- TEMP_FILE_MAX_AGE_MS: parseInt(process.env.TEMP_FILE_MAX_AGE_MS || '60000'), // Delete temp files older than 60 seconds
43
-
44
- // Default paths
45
- DEFAULT_CWD: '/workspace',
46
- TEMP_DIR: '/tmp'
47
- } as const;
48
-
49
- // Types
50
- // Internal execution result from the isolated process
51
- export interface RawExecResult {
52
- stdout: string;
53
- stderr: string;
54
- exitCode: number;
55
- }
56
-
57
- export interface SessionOptions {
58
- id: string;
59
- env?: Record<string, string>;
60
- cwd?: string;
61
- isolation?: boolean;
62
- }
63
-
64
- interface ControlMessage {
65
- type: 'exec' | 'exec_stream' | 'exit';
66
- id: string;
67
- command?: string;
68
- cwd?: string;
69
- }
70
-
71
- interface ControlResponse {
72
- type: 'result' | 'error' | 'ready' | 'stream_event';
73
- id: string;
74
- stdout?: string;
75
- stderr?: string;
76
- exitCode?: number;
77
- error?: string;
78
- event?: ExecEvent;
79
- }
80
-
81
- // Cache the namespace support check
82
- let namespaceSupport: boolean | null = null;
83
-
84
- /**
85
- * Check if PID namespace isolation is available (requires CAP_SYS_ADMIN).
86
- * Returns true in production, false in typical development environments.
87
- */
88
- export function hasNamespaceSupport(): boolean {
89
- if (namespaceSupport !== null) {
90
- return namespaceSupport;
91
- }
92
-
93
- try {
94
- // Actually test if unshare works
95
- const { execSync } = require('node:child_process');
96
- execSync('unshare --pid --fork --mount-proc true', {
97
- stdio: 'ignore',
98
- timeout: 1000
99
- });
100
- console.log('[Isolation] Namespace support detected (CAP_SYS_ADMIN available)');
101
- namespaceSupport = true;
102
- return true;
103
- } catch (error) {
104
- console.log('[Isolation] No namespace support (CAP_SYS_ADMIN not available)');
105
- namespaceSupport = false;
106
- return false;
107
- }
108
- }
109
-
110
- /**
111
- * Session with isolated command execution.
112
- * Maintains state across commands within the session.
113
- */
114
- export class Session {
115
- private control: ChildProcess | null = null;
116
- private ready = false;
117
- private canIsolate: boolean;
118
- private pendingCallbacks = new Map<string, {
119
- resolve: (result: RawExecResult) => void;
120
- reject: (error: Error) => void;
121
- timeout: NodeJS.Timeout;
122
- }>();
123
- private processes = new Map<string, ProcessRecord>(); // Session-specific processes
124
-
125
- constructor(private options: SessionOptions) {
126
- this.canIsolate = (options.isolation === true) && hasNamespaceSupport();
127
- if (options.isolation === true && !this.canIsolate) {
128
- console.log(`[Session] Isolation requested for '${options.id}' but not available`);
129
- }
130
- }
131
-
132
- /**
133
- * Check if the session is ready for command execution
134
- */
135
- isReady(): boolean {
136
- return this.ready && this.control !== null && !this.control.killed;
137
- }
138
-
139
- async initialize(): Promise<void> {
140
- // Use the proper TypeScript control process file
141
- const controlProcessPath = path.join(__dirname, 'control-process.js');
142
-
143
- // Start control process with configuration via environment variables
144
- this.control = spawn('node', [controlProcessPath], {
145
- stdio: ['pipe', 'pipe', 'pipe'],
146
- env: {
147
- ...process.env,
148
- SESSION_ID: this.options.id,
149
- SESSION_CWD: this.options.cwd || CONFIG.DEFAULT_CWD,
150
- SESSION_ISOLATED: this.canIsolate ? '1' : '0',
151
- COMMAND_TIMEOUT_MS: String(CONFIG.COMMAND_TIMEOUT_MS),
152
- CLEANUP_INTERVAL_MS: String(CONFIG.CLEANUP_INTERVAL_MS),
153
- TEMP_FILE_MAX_AGE_MS: String(CONFIG.TEMP_FILE_MAX_AGE_MS),
154
- TEMP_DIR: CONFIG.TEMP_DIR,
155
- ...this.options.env
156
- }
157
- });
158
-
159
- // Handle control process output
160
- this.control.stdout?.on('data', (data: Buffer) => {
161
- const lines = data.toString().split('\n');
162
- for (const line of lines) {
163
- if (!line.trim()) continue;
164
- try {
165
- const msg: ControlResponse = JSON.parse(line);
166
- this.handleControlMessage(msg);
167
- } catch (e) {
168
- console.error(`[Session] Failed to parse control message: ${line}`);
169
- }
170
- }
171
- });
172
-
173
- // Handle control process errors
174
- this.control.stderr?.on('data', (data: Buffer) => {
175
- console.error(`[Session] Control stderr for '${this.options.id}': ${data.toString()}`);
176
- });
177
-
178
- this.control.on('error', (error) => {
179
- console.error(`[Session] Control process error for '${this.options.id}':`, error);
180
- this.cleanup(error);
181
- });
182
-
183
- this.control.on('exit', (code) => {
184
- console.log(`[Session] Control process exited for '${this.options.id}' with code ${code}`);
185
- this.cleanup(new Error(`Control process exited with code ${code}`));
186
- });
187
-
188
- // Wait for ready signal
189
- await this.waitForReady();
190
-
191
- console.log(`[Session] Session '${this.options.id}' initialized successfully`);
192
- }
193
-
194
-
195
- private waitForReady(): Promise<void> {
196
- return new Promise((resolve, reject) => {
197
- const timeout = setTimeout(() => {
198
- reject(new Error('Control process initialization timeout'));
199
- }, CONFIG.READY_TIMEOUT_MS);
200
-
201
- const checkReady = (msg: ControlResponse) => {
202
- if (msg.type === 'ready' && msg.id === 'init') {
203
- clearTimeout(timeout);
204
- this.ready = true;
205
- resolve();
206
- }
207
- };
208
-
209
- // Temporarily store the ready handler
210
- const originalHandler = this.handleControlMessage;
211
- this.handleControlMessage = (msg) => {
212
- checkReady(msg);
213
- originalHandler.call(this, msg);
214
- };
215
- });
216
- }
217
-
218
- private handleControlMessage(msg: ControlResponse): void {
219
- if (msg.type === 'ready' && msg.id === 'init') {
220
- this.ready = true;
221
- return;
222
- }
223
-
224
- const callback = this.pendingCallbacks.get(msg.id);
225
- if (!callback) return;
226
-
227
- clearTimeout(callback.timeout);
228
- this.pendingCallbacks.delete(msg.id);
229
-
230
- if (msg.type === 'error') {
231
- callback.reject(new Error(msg.error || 'Unknown error'));
232
- } else if (msg.type === 'result') {
233
- callback.resolve({
234
- stdout: msg.stdout || '',
235
- stderr: msg.stderr || '',
236
- exitCode: msg.exitCode || 0
237
- });
238
- }
239
- }
240
-
241
- private cleanup(error?: Error): void {
242
- // Reject all pending callbacks
243
- for (const [id, callback] of this.pendingCallbacks) {
244
- clearTimeout(callback.timeout);
245
- callback.reject(error || new Error('Session terminated'));
246
- }
247
- this.pendingCallbacks.clear();
248
-
249
- // Kill control process if still running
250
- if (this.control && !this.control.killed) {
251
- this.control.kill('SIGTERM');
252
- }
253
-
254
- this.control = null;
255
- this.ready = false;
256
- }
257
-
258
- async exec(command: string, options?: { cwd?: string }): Promise<ExecResult> {
259
- if (!this.ready || !this.control) {
260
- throw new Error(`Session '${this.options.id}' not initialized`);
261
- }
262
-
263
- // Validate cwd if provided - must be absolute path
264
- if (options?.cwd) {
265
- if (!options.cwd.startsWith('/')) {
266
- throw new Error(`cwd must be an absolute path starting with '/', got: ${options.cwd}`);
267
- }
268
- }
269
-
270
- const id = randomUUID();
271
- const startTime = Date.now();
272
-
273
- return new Promise<RawExecResult>((resolve, reject) => {
274
- // Set up timeout
275
- const timeout = setTimeout(() => {
276
- this.pendingCallbacks.delete(id);
277
- reject(new Error(`Command timeout: ${command}`));
278
- }, CONFIG.COMMAND_TIMEOUT_MS);
279
-
280
- // Store callback
281
- this.pendingCallbacks.set(id, { resolve, reject, timeout });
282
-
283
- // Send command to control process with cwd override
284
- const msg: ControlMessage = {
285
- type: 'exec',
286
- id,
287
- command,
288
- cwd: options?.cwd // Pass through cwd override if provided
289
- };
290
- this.control!.stdin?.write(`${JSON.stringify(msg)}\n`);
291
- }).then(raw => ({
292
- ...raw,
293
- success: raw.exitCode === 0,
294
- command,
295
- duration: Date.now() - startTime,
296
- timestamp: new Date().toISOString()
297
- }));
298
- }
299
-
300
- async *execStream(command: string, options?: { cwd?: string }): AsyncGenerator<ExecEvent> {
301
- if (!this.ready || !this.control) {
302
- throw new Error(`Session '${this.options.id}' not initialized`);
303
- }
304
-
305
- // Validate cwd if provided - must be absolute path
306
- if (options?.cwd) {
307
- if (!options.cwd.startsWith('/')) {
308
- throw new Error(`cwd must be an absolute path starting with '/', got: ${options.cwd}`);
309
- }
310
- }
311
-
312
- const id = randomUUID();
313
- const timestamp = new Date().toISOString();
314
-
315
- // Yield start event
316
- yield {
317
- type: 'start',
318
- timestamp,
319
- command,
320
- };
321
-
322
- // Set up streaming callback handling
323
- const streamingCallbacks = new Map<string, {
324
- resolve: () => void;
325
- reject: (error: Error) => void;
326
- events: ExecEvent[];
327
- complete: boolean;
328
- }>();
329
-
330
- // Temporarily override message handling to capture streaming events
331
- const originalHandler = this.control!.stdout?.listeners('data')[0];
332
-
333
- const streamHandler = (data: Buffer) => {
334
- const lines = data.toString().split('\n');
335
- for (const line of lines) {
336
- if (!line.trim()) continue;
337
- try {
338
- const msg: any = JSON.parse(line);
339
- if (msg.type === 'stream_event' && msg.id === id) {
340
- const callback = streamingCallbacks.get(id);
341
- if (callback) {
342
- callback.events.push(msg.event);
343
- if (msg.event.type === 'complete' || msg.event.type === 'error') {
344
- callback.complete = true;
345
- callback.resolve();
346
- }
347
- }
348
- } else {
349
- // Pass through other messages to original handler
350
- originalHandler?.(data);
351
- }
352
- } catch (e) {
353
- console.error(`[Session] Failed to parse stream message: ${line}`);
354
- }
355
- }
356
- };
357
-
358
- try {
359
- // Set up promise for completion
360
- const streamPromise = new Promise<void>((resolve, reject) => {
361
- streamingCallbacks.set(id, {
362
- resolve,
363
- reject,
364
- events: [],
365
- complete: false
366
- });
367
-
368
- // Set up timeout
369
- setTimeout(() => {
370
- const callback = streamingCallbacks.get(id);
371
- if (callback && !callback.complete) {
372
- streamingCallbacks.delete(id);
373
- reject(new Error(`Stream timeout: ${command}`));
374
- }
375
- }, CONFIG.COMMAND_TIMEOUT_MS);
376
- });
377
-
378
- // Replace stdout handler temporarily
379
- this.control!.stdout?.off('data', originalHandler as any);
380
- this.control!.stdout?.on('data', streamHandler);
381
-
382
- // Send streaming exec command to control process
383
- const msg: ControlMessage = {
384
- type: 'exec_stream',
385
- id,
386
- command,
387
- cwd: options?.cwd // Pass through cwd override if provided
388
- };
389
- this.control!.stdin?.write(`${JSON.stringify(msg)}\n`);
390
-
391
- // Yield events as they come in
392
- const callback = streamingCallbacks.get(id)!;
393
- let lastEventIndex = 0;
394
-
395
- while (!callback.complete) {
396
- // Yield any new events
397
- while (lastEventIndex < callback.events.length) {
398
- yield callback.events[lastEventIndex];
399
- lastEventIndex++;
400
- }
401
-
402
- // Wait a bit before checking again
403
- await new Promise(resolve => setTimeout(resolve, 10));
404
- }
405
-
406
- // Yield any remaining events
407
- while (lastEventIndex < callback.events.length) {
408
- yield callback.events[lastEventIndex];
409
- lastEventIndex++;
410
- }
411
-
412
- await streamPromise;
413
-
414
- } catch (error) {
415
- yield {
416
- type: 'error',
417
- timestamp: new Date().toISOString(),
418
- command,
419
- error: error instanceof Error ? error.message : String(error),
420
- };
421
- } finally {
422
- // Restore original handler
423
- this.control!.stdout?.off('data', streamHandler);
424
- if (originalHandler) {
425
- this.control!.stdout?.on('data', originalHandler as any);
426
- }
427
- streamingCallbacks.delete(id);
428
- }
429
- }
430
-
431
- // File Operations - Execute as shell commands to inherit session context
432
- async writeFileOperation(path: string, content: string, encoding: string = 'utf-8'): Promise<{ success: boolean; exitCode: number; path: string }> {
433
- // Create parent directory if needed, then write file using heredoc
434
- // Note: The quoted heredoc delimiter 'SANDBOX_EOF' prevents variable expansion
435
- // and treats the content literally, so no escaping is required
436
- const command = `mkdir -p "$(dirname "${path}")" && cat > "${path}" << 'SANDBOX_EOF'
437
- ${content}
438
- SANDBOX_EOF`;
439
-
440
- const result = await this.exec(command);
441
-
442
- return {
443
- success: result.exitCode === 0,
444
- exitCode: result.exitCode,
445
- path
446
- };
447
- }
448
-
449
- async readFileOperation(path: string, encoding: string = 'utf-8'): Promise<{
450
- success: boolean;
451
- exitCode: number;
452
- content: string;
453
- path: string;
454
- encoding?: 'utf-8' | 'base64';
455
- isBinary?: boolean;
456
- mimeType?: string;
457
- size?: number;
458
- }> {
459
- // Step 1: Check if file exists and get metadata
460
- const statCommand = `stat -c '%s' "${path}" 2>/dev/null || echo "FILE_NOT_FOUND"`;
461
- const statResult = await this.exec(statCommand);
462
-
463
- if (statResult.stdout.trim() === 'FILE_NOT_FOUND') {
464
- // File doesn't exist - return error
465
- return {
466
- success: false,
467
- exitCode: 1,
468
- content: '',
469
- path
470
- };
471
- }
472
-
473
- const fileSize = parseInt(statResult.stdout.trim(), 10);
474
-
475
- // Step 2: Detect MIME type using file command
476
- const mimeCommand = `file --mime-type -b "${path}"`;
477
- const mimeResult = await this.exec(mimeCommand);
478
- const mimeType = mimeResult.stdout.trim();
479
-
480
- // Step 3: Determine if file is binary based on MIME type
481
- // Text MIME types: text/*, application/json, application/xml, application/javascript, etc.
482
- const isBinary = !mimeType.startsWith('text/') &&
483
- !mimeType.includes('json') &&
484
- !mimeType.includes('xml') &&
485
- !mimeType.includes('javascript') &&
486
- !mimeType.includes('x-empty');
487
-
488
- // Step 4: Read file with appropriate encoding
489
- let content: string;
490
- let actualEncoding: 'utf-8' | 'base64';
491
-
492
- if (isBinary) {
493
- // Use base64 for binary files
494
- const base64Command = `base64 -w 0 "${path}"`;
495
- const base64Result = await this.exec(base64Command);
496
- content = base64Result.stdout;
497
- actualEncoding = 'base64';
498
- } else {
499
- // Use cat for text files
500
- const catCommand = `cat "${path}"`;
501
- const catResult = await this.exec(catCommand);
502
- content = catResult.stdout;
503
- actualEncoding = 'utf-8';
504
- }
505
-
506
- return {
507
- success: true,
508
- exitCode: 0,
509
- content,
510
- path,
511
- encoding: actualEncoding,
512
- isBinary,
513
- mimeType,
514
- size: fileSize
515
- };
516
- }
517
-
518
- async readFileStreamOperation(path: string): Promise<ReadableStream<Uint8Array>> {
519
- const encoder = new TextEncoder();
520
-
521
- // Helper to send SSE event
522
- const sseEvent = (event: any): Uint8Array => {
523
- return encoder.encode(`data: ${JSON.stringify(event)}\n\n`);
524
- };
525
-
526
- // Create streaming response
527
- return new ReadableStream({
528
- start: async (controller) => {
529
- try {
530
- // Step 1: Get file metadata (same logic as readFileOperation)
531
- const statCommand = `stat -c '%s' "${path}" 2>/dev/null || echo "FILE_NOT_FOUND"`;
532
- const statResult = await this.exec(statCommand);
533
-
534
- if (statResult.stdout.trim() === 'FILE_NOT_FOUND') {
535
- // File doesn't exist - send error event
536
- controller.enqueue(sseEvent({
537
- type: 'error',
538
- error: `File not found: ${path}`
539
- }));
540
- controller.close();
541
- return;
542
- }
543
-
544
- const fileSize = parseInt(statResult.stdout.trim(), 10);
545
-
546
- // Step 2: Detect MIME type
547
- const mimeCommand = `file --mime-type -b "${path}"`;
548
- const mimeResult = await this.exec(mimeCommand);
549
- const mimeType = mimeResult.stdout.trim();
550
-
551
- // Step 3: Determine if binary
552
- const isBinary = !mimeType.startsWith('text/') &&
553
- !mimeType.includes('json') &&
554
- !mimeType.includes('xml') &&
555
- !mimeType.includes('javascript') &&
556
- !mimeType.includes('x-empty');
557
-
558
- const encoding: 'utf-8' | 'base64' = isBinary ? 'base64' : 'utf-8';
559
-
560
- // Step 4: Send metadata event
561
- controller.enqueue(sseEvent({
562
- type: 'metadata',
563
- mimeType,
564
- size: fileSize,
565
- isBinary,
566
- encoding
567
- }));
568
-
569
- // Step 5: Stream file in chunks
570
- // IMPORTANT: Chunk size MUST be divisible by 3 for base64 encoding!
571
- // Base64 encodes 3 bytes at a time. If chunks aren't aligned,
572
- // concatenating separately-encoded base64 strings corrupts the data.
573
- const CHUNK_SIZE = 65535;
574
- let bytesRead = 0;
575
-
576
- while (bytesRead < fileSize) {
577
- const remainingBytes = fileSize - bytesRead;
578
- const chunkSize = Math.min(CHUNK_SIZE, remainingBytes);
579
-
580
- // Use dd to read chunk at specific offset
581
- // bs=1 means 1 byte block size, skip=offset, count=chunkSize
582
- let chunkCommand: string;
583
- if (isBinary) {
584
- // For binary, read and encode as base64
585
- chunkCommand = `dd if="${path}" bs=1 skip=${bytesRead} count=${chunkSize} 2>/dev/null | base64 -w 0`;
586
- } else {
587
- // For text, just read
588
- chunkCommand = `dd if="${path}" bs=1 skip=${bytesRead} count=${chunkSize} 2>/dev/null`;
589
- }
590
-
591
- const chunkResult = await this.exec(chunkCommand);
592
-
593
- // Send chunk event
594
- controller.enqueue(sseEvent({
595
- type: 'chunk',
596
- data: chunkResult.stdout
597
- }));
598
-
599
- bytesRead += chunkSize;
600
- }
601
-
602
- // Step 6: Send complete event
603
- controller.enqueue(sseEvent({
604
- type: 'complete',
605
- bytesRead
606
- }));
607
-
608
- controller.close();
609
- } catch (error) {
610
- // Send error event
611
- controller.enqueue(sseEvent({
612
- type: 'error',
613
- error: error instanceof Error ? error.message : String(error)
614
- }));
615
- controller.close();
616
- }
617
- },
618
-
619
- cancel() {
620
- console.log(`[Session] File stream cancelled for: ${path}`);
621
- }
622
- });
623
- }
624
-
625
- async mkdirOperation(path: string, recursive: boolean = false): Promise<{ success: boolean; exitCode: number; path: string; recursive: boolean }> {
626
- const command = recursive ? `mkdir -p "${path}"` : `mkdir "${path}"`;
627
- const result = await this.exec(command);
628
-
629
- return {
630
- success: result.exitCode === 0,
631
- exitCode: result.exitCode,
632
- path,
633
- recursive
634
- };
635
- }
636
-
637
- async deleteFileOperation(path: string): Promise<{ success: boolean; exitCode: number; path: string }> {
638
- const command = `rm "${path}"`;
639
- const result = await this.exec(command);
640
-
641
- return {
642
- success: result.exitCode === 0,
643
- exitCode: result.exitCode,
644
- path
645
- };
646
- }
647
-
648
- async renameFileOperation(oldPath: string, newPath: string): Promise<{ success: boolean; exitCode: number; oldPath: string; newPath: string }> {
649
- const command = `mv "${oldPath}" "${newPath}"`;
650
- const result = await this.exec(command);
651
-
652
- return {
653
- success: result.exitCode === 0,
654
- exitCode: result.exitCode,
655
- oldPath,
656
- newPath
657
- };
658
- }
659
-
660
- async moveFileOperation(sourcePath: string, destinationPath: string): Promise<{ success: boolean; exitCode: number; sourcePath: string; destinationPath: string }> {
661
- const command = `mv "${sourcePath}" "${destinationPath}"`;
662
- const result = await this.exec(command);
663
-
664
- return {
665
- success: result.exitCode === 0,
666
- exitCode: result.exitCode,
667
- sourcePath,
668
- destinationPath
669
- };
670
- }
671
-
672
- async listFilesOperation(path: string, options?: { recursive?: boolean; includeHidden?: boolean }): Promise<{ success: boolean; exitCode: number; files: any[]; path: string }> {
673
- // Build ls command with appropriate flags
674
- let lsFlags = '-la'; // Long format with all files (including hidden)
675
- if (!options?.includeHidden) {
676
- lsFlags = '-l'; // Long format without hidden files
677
- }
678
- if (options?.recursive) {
679
- lsFlags += 'R'; // Recursive
680
- }
681
-
682
- const command = `ls ${lsFlags} "${path}" 2>/dev/null || echo "DIRECTORY_NOT_FOUND"`;
683
- const result = await this.exec(command);
684
-
685
- if (result.stdout.includes('DIRECTORY_NOT_FOUND')) {
686
- return {
687
- success: false,
688
- exitCode: 1,
689
- files: [],
690
- path
691
- };
692
- }
693
-
694
- // Parse ls output into structured file data
695
- const files = this.parseLsOutput(result.stdout, path);
696
-
697
- return {
698
- success: result.exitCode === 0,
699
- exitCode: result.exitCode,
700
- files,
701
- path
702
- };
703
- }
704
-
705
- private parseLsOutput(lsOutput: string, basePath: string): any[] {
706
- const lines = lsOutput.split('\n').filter(line => line.trim());
707
- const files: any[] = [];
708
-
709
- for (const line of lines) {
710
- // Skip total line and empty lines
711
- if (line.startsWith('total') || !line.trim()) continue;
712
-
713
- // Parse ls -l format: permissions, links, user, group, size, date, name
714
- const match = line.match(/^([-dlrwx]+)\s+\d+\s+\S+\s+\S+\s+(\d+)\s+(\S+\s+\d+\s+[\d:]+)\s+(.+)$/);
715
- if (!match) continue;
716
-
717
- const [, permissions, size, dateStr, name] = match;
718
-
719
- // Determine file type from first character of permissions
720
- let type: 'file' | 'directory' | 'symlink' | 'other' = 'file';
721
- if (permissions.startsWith('d')) type = 'directory';
722
- else if (permissions.startsWith('l')) type = 'symlink';
723
- else if (!permissions.startsWith('-')) type = 'other';
724
-
725
- // Extract permission booleans for user (first 3 chars after type indicator)
726
- const userPerms = permissions.substring(1, 4);
727
-
728
- files.push({
729
- name,
730
- absolutePath: `${basePath}/${name}`,
731
- relativePath: name,
732
- type,
733
- size: parseInt(size),
734
- modifiedAt: dateStr, // Simplified date parsing
735
- mode: permissions,
736
- permissions: {
737
- readable: userPerms[0] === 'r',
738
- writable: userPerms[1] === 'w',
739
- executable: userPerms[2] === 'x'
740
- }
741
- });
742
- }
743
-
744
- return files;
745
- }
746
-
747
- // Process Management Methods
748
-
749
- async startProcess(command: string, options?: {
750
- processId?: string;
751
- timeout?: number;
752
- env?: Record<string, string>;
753
- cwd?: string;
754
- encoding?: string;
755
- autoCleanup?: boolean;
756
- }): Promise<ProcessRecord> {
757
- // Validate cwd if provided - must be absolute path
758
- if (options?.cwd) {
759
- if (!options.cwd.startsWith('/')) {
760
- throw new Error(`cwd must be an absolute path starting with '/', got: ${options.cwd}`);
761
- }
762
- }
763
-
764
- const processId = options?.processId || `proc_${Date.now()}_${randomBytes(6).toString('hex')}`;
765
-
766
- // Check if process ID already exists in this session
767
- if (this.processes.has(processId)) {
768
- throw new Error(`Process already exists in session: ${processId}`);
769
- }
770
-
771
- console.log(`[Session ${this.options.id}] Starting process: ${command} (ID: ${processId})`);
772
-
773
- // Create separate temp files for stdout and stderr
774
- const stdoutFile = `/tmp/proc_${processId}.stdout`;
775
- const stderrFile = `/tmp/proc_${processId}.stderr`;
776
-
777
- // Create process record
778
- const processRecord: ProcessRecord = {
779
- id: processId,
780
- command,
781
- status: 'starting',
782
- startTime: new Date(),
783
- stdout: '',
784
- stderr: '',
785
- outputListeners: new Set(),
786
- statusListeners: new Set()
787
- };
788
-
789
- this.processes.set(processId, processRecord);
790
-
791
- // Start the process in background with nohup
792
- // Keep stdout and stderr separate
793
- const backgroundCommand = `nohup ${command} > ${stdoutFile} 2> ${stderrFile} & echo $!`;
794
-
795
- try {
796
- // Execute the background command and get PID directly
797
- const result = await this.exec(backgroundCommand, { cwd: options?.cwd });
798
- const pid = parseInt(result.stdout.trim(), 10);
799
-
800
- if (Number.isNaN(pid)) {
801
- throw new Error(`Failed to get process PID from output: ${result.stdout}`);
802
- }
803
-
804
- processRecord.pid = pid;
805
- processRecord.status = 'running';
806
-
807
- // Store the output file paths for later retrieval
808
- processRecord.stdoutFile = stdoutFile;
809
- processRecord.stderrFile = stderrFile;
810
-
811
- console.log(`[Session ${this.options.id}] Process ${processId} started with PID ${pid}`);
812
-
813
- } catch (error) {
814
- processRecord.status = 'error';
815
- processRecord.endTime = new Date();
816
- processRecord.stderr = `Failed to start process: ${error instanceof Error ? error.message : String(error)}`;
817
-
818
- // Notify status listeners
819
- for (const listener of processRecord.statusListeners) {
820
- listener('error');
821
- }
822
-
823
- // Clean up temp files
824
- this.exec(`rm -f ${stdoutFile} ${stderrFile}`).catch(() => {});
825
-
826
- throw error;
827
- }
828
-
829
- return processRecord;
830
- }
831
-
832
- // Check and update process status on-demand
833
- private async updateProcessStatus(processRecord: ProcessRecord): Promise<void> {
834
- if (!processRecord.pid || processRecord.status !== 'running') {
835
- return; // Nothing to check
836
- }
837
-
838
- try {
839
- // Check if process is still running (kill -0 just checks, doesn't actually kill)
840
- const checkResult = await this.exec(`kill -0 ${processRecord.pid} 2>/dev/null && echo "running" || echo "stopped"`);
841
- const isRunning = checkResult.stdout.trim() === 'running';
842
-
843
- if (!isRunning) {
844
- // Process has stopped
845
- processRecord.status = 'completed';
846
- processRecord.endTime = new Date();
847
-
848
- // Try to get exit status from wait (may not work if process already reaped)
849
- try {
850
- const waitResult = await this.exec(`wait ${processRecord.pid} 2>/dev/null; echo $?`);
851
- const exitCode = parseInt(waitResult.stdout.trim(), 10);
852
- if (!Number.isNaN(exitCode)) {
853
- processRecord.exitCode = exitCode;
854
- if (exitCode !== 0) {
855
- processRecord.status = 'failed';
856
- }
857
- }
858
- } catch {
859
- // Can't get exit code, that's okay
860
- }
861
-
862
- // Read final output if not already cached
863
- if ((processRecord.stdoutFile || processRecord.stderrFile) && (!processRecord.stdout && !processRecord.stderr)) {
864
- await this.updateProcessLogs(processRecord);
865
- }
866
-
867
- // Notify status listeners
868
- for (const listener of processRecord.statusListeners) {
869
- listener(processRecord.status);
870
- }
871
-
872
- console.log(`[Session ${this.options.id}] Process ${processRecord.id} completed with status: ${processRecord.status}`);
873
- }
874
- } catch (error) {
875
- console.error(`[Session ${this.options.id}] Error checking process ${processRecord.id} status:`, error);
876
- }
877
- }
878
-
879
- // Update process logs from output files
880
- private async updateProcessLogs(processRecord: ProcessRecord): Promise<void> {
881
- if (!processRecord.stdoutFile && !processRecord.stderrFile) {
882
- return;
883
- }
884
-
885
- try {
886
- // Read stdout
887
- if (processRecord.stdoutFile) {
888
- const result = await this.exec(`cat ${processRecord.stdoutFile} 2>/dev/null || true`);
889
- const newStdout = result.stdout;
890
-
891
- // Check if there's new stdout
892
- if (newStdout.length > processRecord.stdout.length) {
893
- const delta = newStdout.substring(processRecord.stdout.length);
894
- processRecord.stdout = newStdout;
895
-
896
- // Notify output listeners if any
897
- for (const listener of processRecord.outputListeners) {
898
- listener('stdout', delta);
899
- }
900
- }
901
- }
902
-
903
- // Read stderr
904
- if (processRecord.stderrFile) {
905
- const result = await this.exec(`cat ${processRecord.stderrFile} 2>/dev/null || true`);
906
- const newStderr = result.stdout; // Note: reading stderr file content from result.stdout
907
-
908
- // Check if there's new stderr
909
- if (newStderr.length > processRecord.stderr.length) {
910
- const delta = newStderr.substring(processRecord.stderr.length);
911
- processRecord.stderr = newStderr;
912
-
913
- // Notify output listeners if any
914
- for (const listener of processRecord.outputListeners) {
915
- listener('stderr', delta);
916
- }
917
- }
918
- }
919
- } catch (error) {
920
- console.error(`[Session ${this.options.id}] Error reading process ${processRecord.id} logs:`, error);
921
- }
922
- }
923
-
924
- async getProcess(processId: string): Promise<ProcessRecord | undefined> {
925
- const process = this.processes.get(processId);
926
- if (process) {
927
- // Update status before returning
928
- await this.updateProcessStatus(process);
929
- }
930
- return process;
931
- }
932
-
933
- async listProcesses(): Promise<ProcessRecord[]> {
934
- const processes = Array.from(this.processes.values());
935
-
936
- // Update status for all running processes
937
- for (const process of processes) {
938
- if (process.status === 'running') {
939
- await this.updateProcessStatus(process);
940
- }
941
- }
942
-
943
- return processes;
944
- }
945
-
946
- async killProcess(processId: string): Promise<boolean> {
947
- const process = this.processes.get(processId);
948
- if (!process) {
949
- return false;
950
- }
951
-
952
- // Stop monitoring first
953
- this.stopProcessMonitoring(process);
954
-
955
- // Only try to kill if process is running
956
- if (process.pid && (process.status === 'running' || process.status === 'starting')) {
957
- try {
958
- // Send SIGTERM to the process
959
- await this.exec(`kill ${process.pid} 2>/dev/null || true`);
960
- console.log(`[Session ${this.options.id}] Sent SIGTERM to process ${processId} (PID: ${process.pid})`);
961
-
962
- // Give it a moment to terminate gracefully (500ms), then force kill if needed
963
- await new Promise(resolve => setTimeout(resolve, 500));
964
-
965
- // Check if still running and force kill if needed
966
- const checkResult = await this.exec(`kill -0 ${process.pid} 2>/dev/null && echo "running" || echo "stopped"`);
967
- if (checkResult.stdout.trim() === 'running') {
968
- await this.exec(`kill -9 ${process.pid} 2>/dev/null || true`);
969
- console.log(`[Session ${this.options.id}] Force killed process ${processId} (PID: ${process.pid})`);
970
- }
971
- } catch (error) {
972
- console.error(`[Session ${this.options.id}] Error killing process ${processId}:`, error);
973
- }
974
-
975
- process.status = 'killed';
976
- process.endTime = new Date();
977
-
978
- // Read final output before cleanup
979
- if (process.stdoutFile || process.stderrFile) {
980
- await this.updateProcessLogs(process);
981
- }
982
-
983
- // Notify status listeners
984
- for (const listener of process.statusListeners) {
985
- listener('killed');
986
- }
987
-
988
- // Clean up temp files
989
- if (process.stdoutFile || process.stderrFile) {
990
- this.exec(`rm -f ${process.stdoutFile} ${process.stderrFile}`).catch(() => {});
991
- }
992
-
993
- return true;
994
- }
995
-
996
- // Process not running, just update status
997
- if (process.status === 'running' || process.status === 'starting') {
998
- process.status = 'killed';
999
- process.endTime = new Date();
1000
-
1001
- // Notify status listeners
1002
- for (const listener of process.statusListeners) {
1003
- listener('killed');
1004
- }
1005
- }
1006
-
1007
- return true;
1008
- }
1009
-
1010
- async killAllProcesses(): Promise<number> {
1011
- let killedCount = 0;
1012
- for (const [id, _] of this.processes) {
1013
- if (await this.killProcess(id)) {
1014
- killedCount++;
1015
- }
1016
- }
1017
- return killedCount;
1018
- }
1019
-
1020
- // Get process logs (updates from files if needed)
1021
- async getProcessLogs(processId: string): Promise<{ stdout: string; stderr: string }> {
1022
- const process = this.processes.get(processId);
1023
- if (!process) {
1024
- throw new Error(`Process not found: ${processId}`);
1025
- }
1026
-
1027
- // Update logs from files
1028
- if (process.stdoutFile || process.stderrFile) {
1029
- await this.updateProcessLogs(process);
1030
- }
1031
-
1032
- // Return current logs
1033
- return {
1034
- stdout: process.stdout,
1035
- stderr: process.stderr
1036
- };
1037
- }
1038
-
1039
- // Start monitoring a process for output changes
1040
- startProcessMonitoring(processRecord: ProcessRecord): void {
1041
- // Don't monitor if already monitoring or process not running
1042
- if (processRecord.monitoringInterval || processRecord.status !== 'running') {
1043
- return;
1044
- }
1045
-
1046
- console.log(`[Session ${this.options.id}] Starting monitoring for process ${processRecord.id}`);
1047
-
1048
- // Poll every 100ms for near real-time updates
1049
- processRecord.monitoringInterval = setInterval(async () => {
1050
- // Stop monitoring if no listeners or process not running
1051
- if (processRecord.outputListeners.size === 0 || processRecord.status !== 'running') {
1052
- this.stopProcessMonitoring(processRecord);
1053
- return;
1054
- }
1055
-
1056
- // Update logs from temp files (this will notify listeners if there's new content)
1057
- await this.updateProcessLogs(processRecord);
1058
-
1059
- // Also check if process is still running
1060
- await this.updateProcessStatus(processRecord);
1061
- }, 100);
1062
- }
1063
-
1064
- // Stop monitoring a process
1065
- stopProcessMonitoring(processRecord: ProcessRecord): void {
1066
- if (processRecord.monitoringInterval) {
1067
- console.log(`[Session ${this.options.id}] Stopping monitoring for process ${processRecord.id}`);
1068
- clearInterval(processRecord.monitoringInterval);
1069
- processRecord.monitoringInterval = undefined;
1070
- }
1071
- }
1072
-
1073
- async destroy(): Promise<void> {
1074
- // Stop all monitoring intervals first
1075
- for (const process of this.processes.values()) {
1076
- this.stopProcessMonitoring(process);
1077
- }
1078
-
1079
- // Kill all processes
1080
- await this.killAllProcesses();
1081
-
1082
- // Clean up all temp files
1083
- for (const [id, process] of this.processes) {
1084
- if (process.stdoutFile || process.stderrFile) {
1085
- await this.exec(`rm -f ${process.stdoutFile} ${process.stderrFile}`).catch(() => {});
1086
- }
1087
- }
1088
-
1089
- if (this.control) {
1090
- // Send exit command
1091
- const msg: ControlMessage = { type: 'exit', id: 'destroy' };
1092
- this.control.stdin?.write(`${JSON.stringify(msg)}\n`);
1093
-
1094
- // Give it a moment to exit cleanly
1095
- setTimeout(() => {
1096
- if (this.control && !this.control.killed) {
1097
- this.control.kill('SIGTERM');
1098
- }
1099
- }, 100);
1100
-
1101
- this.cleanup();
1102
- }
1103
- }
1104
- }
1105
-
1106
- /**
1107
- * Manages isolated sessions for command execution.
1108
- * Each session maintains its own state (pwd, env vars, processes).
1109
- */
1110
- export class SessionManager {
1111
- private sessions = new Map<string, Session>();
1112
-
1113
- async createSession(options: SessionOptions): Promise<Session> {
1114
- // Validate cwd if provided - must be absolute path
1115
- if (options.cwd) {
1116
- if (!options.cwd.startsWith('/')) {
1117
- throw new Error(`cwd must be an absolute path starting with '/', got: ${options.cwd}`);
1118
- }
1119
- }
1120
-
1121
- // Check if session already exists
1122
- const existing = this.sessions.get(options.id);
1123
- if (existing) {
1124
- // If the existing session is healthy and ready, reuse it
1125
- if (existing.isReady()) {
1126
- console.log(`[SessionManager] Reusing existing session '${options.id}'`);
1127
- return existing;
1128
- }
1129
-
1130
- // If the session exists but is not ready, clean it up and create a new one
1131
- console.log(`[SessionManager] Destroying unhealthy session '${options.id}' before recreating`);
1132
- await existing.destroy();
1133
- }
1134
-
1135
- // Create new session
1136
- const session = new Session(options);
1137
- await session.initialize();
1138
-
1139
- this.sessions.set(options.id, session);
1140
- console.log(`[SessionManager] Created session '${options.id}'`);
1141
- return session;
1142
- }
1143
-
1144
- getSession(id: string): Session | undefined {
1145
- return this.sessions.get(id);
1146
- }
1147
-
1148
- listSessions(): string[] {
1149
- return Array.from(this.sessions.keys());
1150
- }
1151
-
1152
- // Helper to get or create default session - reduces duplication
1153
- async getOrCreateDefaultSession(): Promise<Session> {
1154
- return await this.createSession({
1155
- id: 'default',
1156
- cwd: '/workspace', // Consistent default working directory
1157
- isolation: true
1158
- });
1159
- }
1160
-
1161
- async exec(command: string, options?: { cwd?: string }): Promise<ExecResult> {
1162
- const defaultSession = await this.getOrCreateDefaultSession();
1163
- return defaultSession.exec(command, options);
1164
- }
1165
-
1166
- async *execStream(command: string, options?: { cwd?: string }): AsyncGenerator<ExecEvent> {
1167
- const defaultSession = await this.getOrCreateDefaultSession();
1168
- yield* defaultSession.execStream(command, options);
1169
- }
1170
-
1171
- // File Operations - Clean method names following existing pattern
1172
- async writeFile(path: string, content: string, encoding?: string): Promise<{ success: boolean; exitCode: number; path: string }> {
1173
- const defaultSession = await this.getOrCreateDefaultSession();
1174
- return defaultSession.writeFileOperation(path, content, encoding);
1175
- }
1176
-
1177
- async readFile(path: string, encoding?: string): Promise<{ success: boolean; exitCode: number; content: string; path: string; encoding?: 'utf-8' | 'base64'; isBinary?: boolean; mimeType?: string; size?: number }> {
1178
- const defaultSession = await this.getOrCreateDefaultSession();
1179
- return defaultSession.readFileOperation(path, encoding);
1180
- }
1181
-
1182
- async mkdir(path: string, recursive?: boolean): Promise<{ success: boolean; exitCode: number; path: string; recursive: boolean }> {
1183
- const defaultSession = await this.getOrCreateDefaultSession();
1184
- return defaultSession.mkdirOperation(path, recursive);
1185
- }
1186
-
1187
- async deleteFile(path: string): Promise<{ success: boolean; exitCode: number; path: string }> {
1188
- const defaultSession = await this.getOrCreateDefaultSession();
1189
- return defaultSession.deleteFileOperation(path);
1190
- }
1191
-
1192
- async renameFile(oldPath: string, newPath: string): Promise<{ success: boolean; exitCode: number; oldPath: string; newPath: string }> {
1193
- const defaultSession = await this.getOrCreateDefaultSession();
1194
- return defaultSession.renameFileOperation(oldPath, newPath);
1195
- }
1196
-
1197
- async moveFile(sourcePath: string, destinationPath: string): Promise<{ success: boolean; exitCode: number; sourcePath: string; destinationPath: string }> {
1198
- const defaultSession = await this.getOrCreateDefaultSession();
1199
- return defaultSession.moveFileOperation(sourcePath, destinationPath);
1200
- }
1201
-
1202
- async listFiles(path: string, options?: { recursive?: boolean; includeHidden?: boolean }): Promise<{ success: boolean; exitCode: number; files: any[]; path: string }> {
1203
- const defaultSession = await this.getOrCreateDefaultSession();
1204
- return defaultSession.listFilesOperation(path, options);
1205
- }
1206
-
1207
- async destroyAll(): Promise<void> {
1208
- for (const session of this.sessions.values()) {
1209
- await session.destroy();
1210
- }
1211
- this.sessions.clear();
1212
- }
1213
- }