@cloudflare/sandbox 0.0.0-db09b4d → 0.0.0-dcf36ef

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 (43) hide show
  1. package/CHANGELOG.md +235 -0
  2. package/Dockerfile +103 -10
  3. package/README.md +859 -22
  4. package/container_src/bun.lock +76 -0
  5. package/container_src/circuit-breaker.ts +121 -0
  6. package/container_src/control-process.ts +784 -0
  7. package/container_src/handler/exec.ts +185 -0
  8. package/container_src/handler/file.ts +457 -0
  9. package/container_src/handler/git.ts +130 -0
  10. package/container_src/handler/ports.ts +314 -0
  11. package/container_src/handler/process.ts +568 -0
  12. package/container_src/handler/session.ts +92 -0
  13. package/container_src/index.ts +447 -2466
  14. package/container_src/interpreter-service.ts +276 -0
  15. package/container_src/isolation.ts +1213 -0
  16. package/container_src/mime-processor.ts +255 -0
  17. package/container_src/package.json +9 -0
  18. package/container_src/runtime/executors/javascript/node_executor.ts +123 -0
  19. package/container_src/runtime/executors/python/ipython_executor.py +338 -0
  20. package/container_src/runtime/executors/typescript/ts_executor.ts +138 -0
  21. package/container_src/runtime/process-pool.ts +464 -0
  22. package/container_src/shell-escape.ts +42 -0
  23. package/container_src/startup.sh +11 -0
  24. package/container_src/types.ts +131 -0
  25. package/package.json +6 -8
  26. package/src/client.ts +510 -1186
  27. package/src/errors.ts +219 -0
  28. package/src/file-stream.ts +162 -0
  29. package/src/index.ts +78 -76
  30. package/src/interpreter-client.ts +352 -0
  31. package/src/interpreter-types.ts +390 -0
  32. package/src/interpreter.ts +150 -0
  33. package/src/request-handler.ts +144 -0
  34. package/src/sandbox.ts +756 -0
  35. package/src/security.ts +113 -0
  36. package/src/sse-parser.ts +147 -0
  37. package/src/types.ts +571 -0
  38. package/tsconfig.json +1 -1
  39. package/tests/client.example.ts +0 -308
  40. package/tests/connection-test.ts +0 -81
  41. package/tests/simple-test.ts +0 -81
  42. package/tests/test1.ts +0 -281
  43. package/tests/test2.ts +0 -710
package/src/client.ts CHANGED
@@ -1,36 +1,23 @@
1
- import type { DurableObject } from "cloudflare:workers";
1
+ import type { ExecuteRequest } from "../container_src/types";
2
2
  import type { Sandbox } from "./index";
3
+ import type {
4
+ BaseExecOptions,
5
+ DeleteFileResponse,
6
+ ExecuteResponse,
7
+ GetProcessLogsResponse,
8
+ GetProcessResponse,
9
+ GitCheckoutResponse,
10
+ ListFilesResponse,
11
+ ListProcessesResponse,
12
+ MkdirResponse,
13
+ MoveFileResponse,
14
+ ReadFileResponse,
15
+ RenameFileResponse,
16
+ StartProcessRequest,
17
+ StartProcessResponse,
18
+ WriteFileResponse,
19
+ } from "./types";
3
20
 
4
- interface ExecuteRequest {
5
- command: string;
6
- args?: string[];
7
- }
8
-
9
- export interface ExecuteResponse {
10
- success: boolean;
11
- stdout: string;
12
- stderr: string;
13
- exitCode: number;
14
- command: string;
15
- args: string[];
16
- timestamp: string;
17
- }
18
-
19
- interface SessionResponse {
20
- sessionId: string;
21
- message: string;
22
- timestamp: string;
23
- }
24
-
25
- interface SessionListResponse {
26
- sessions: Array<{
27
- sessionId: string;
28
- hasActiveProcess: boolean;
29
- createdAt: string;
30
- }>;
31
- count: number;
32
- timestamp: string;
33
- }
34
21
 
35
22
  interface CommandsResponse {
36
23
  availableCommands: string[];
@@ -41,120 +28,103 @@ interface GitCheckoutRequest {
41
28
  repoUrl: string;
42
29
  branch?: string;
43
30
  targetDir?: string;
44
- sessionId?: string;
31
+ sessionId: string;
45
32
  }
46
33
 
47
- export interface GitCheckoutResponse {
48
- success: boolean;
49
- stdout: string;
50
- stderr: string;
51
- exitCode: number;
52
- repoUrl: string;
53
- branch: string;
54
- targetDir: string;
55
- timestamp: string;
56
- }
57
34
 
58
35
  interface MkdirRequest {
59
36
  path: string;
60
37
  recursive?: boolean;
61
- sessionId?: string;
38
+ sessionId: string;
62
39
  }
63
40
 
64
- export interface MkdirResponse {
65
- success: boolean;
66
- stdout: string;
67
- stderr: string;
68
- exitCode: number;
69
- path: string;
70
- recursive: boolean;
71
- timestamp: string;
72
- }
73
41
 
74
42
  interface WriteFileRequest {
75
43
  path: string;
76
44
  content: string;
77
45
  encoding?: string;
78
- sessionId?: string;
46
+ sessionId: string;
79
47
  }
80
48
 
81
- export interface WriteFileResponse {
82
- success: boolean;
83
- exitCode: number;
49
+
50
+ interface ReadFileRequest {
84
51
  path: string;
85
- timestamp: string;
52
+ encoding?: string;
53
+ sessionId: string;
86
54
  }
87
55
 
56
+
88
57
  interface DeleteFileRequest {
89
58
  path: string;
90
- sessionId?: string;
59
+ sessionId: string;
91
60
  }
92
61
 
93
- export interface DeleteFileResponse {
94
- success: boolean;
95
- exitCode: number;
96
- path: string;
97
- timestamp: string;
98
- }
99
62
 
100
63
  interface RenameFileRequest {
101
64
  oldPath: string;
102
65
  newPath: string;
103
- sessionId?: string;
66
+ sessionId: string;
104
67
  }
105
68
 
106
- export interface RenameFileResponse {
107
- success: boolean;
108
- exitCode: number;
109
- oldPath: string;
110
- newPath: string;
111
- timestamp: string;
112
- }
113
69
 
114
70
  interface MoveFileRequest {
115
71
  sourcePath: string;
116
72
  destinationPath: string;
117
- sessionId?: string;
73
+ sessionId: string;
118
74
  }
119
75
 
120
- export interface MoveFileResponse {
76
+
77
+ interface ListFilesRequest {
78
+ path: string;
79
+ options?: {
80
+ recursive?: boolean;
81
+ includeHidden?: boolean;
82
+ };
83
+ sessionId: string;
84
+ }
85
+
86
+
87
+ interface PreviewInfo {
88
+ url: string;
89
+ port: number;
90
+ name?: string;
91
+ }
92
+
93
+ interface ExposedPort extends PreviewInfo {
94
+ exposedAt: string;
95
+ timestamp: string;
96
+ }
97
+
98
+ interface ExposePortResponse {
121
99
  success: boolean;
122
- exitCode: number;
123
- sourcePath: string;
124
- destinationPath: string;
100
+ port: number;
101
+ name?: string;
102
+ exposedAt: string;
125
103
  timestamp: string;
126
104
  }
127
105
 
128
- interface PingResponse {
129
- message: string;
106
+ interface UnexposePortResponse {
107
+ success: boolean;
108
+ port: number;
109
+ timestamp: string;
110
+ }
111
+
112
+ interface GetExposedPortsResponse {
113
+ ports: ExposedPort[];
114
+ count: number;
130
115
  timestamp: string;
131
116
  }
132
117
 
133
- interface StreamEvent {
134
- type: "command_start" | "output" | "command_complete" | "error";
135
- command?: string;
136
- args?: string[];
137
- stream?: "stdout" | "stderr";
138
- data?: string;
139
- message?: string;
140
- path?: string;
141
- oldPath?: string;
142
- newPath?: string;
143
- sourcePath?: string;
144
- destinationPath?: string;
145
- success?: boolean;
146
- exitCode?: number;
147
- stdout?: string;
148
- stderr?: string;
149
- error?: string;
150
- timestamp?: string;
118
+ interface PingResponse {
119
+ message: string;
120
+ timestamp: string;
151
121
  }
152
122
 
153
123
  interface HttpClientOptions {
154
124
  stub?: Sandbox;
155
125
  baseUrl?: string;
156
126
  port?: number;
157
- onCommandStart?: (command: string, args: string[]) => void;
127
+ onCommandStart?: (command: string) => void;
158
128
  onOutput?: (
159
129
  stream: "stdout" | "stderr",
160
130
  data: string,
@@ -165,17 +135,14 @@ interface HttpClientOptions {
165
135
  exitCode: number,
166
136
  stdout: string,
167
137
  stderr: string,
168
- command: string,
169
- args: string[]
138
+ command: string
170
139
  ) => void;
171
- onError?: (error: string, command?: string, args?: string[]) => void;
172
- onStreamEvent?: (event: StreamEvent) => void;
140
+ onError?: (error: string, command?: string) => void;
173
141
  }
174
142
 
175
143
  export class HttpClient {
176
144
  private baseUrl: string;
177
145
  private options: HttpClientOptions;
178
- private sessionId: string | null = null;
179
146
 
180
147
  constructor(options: HttpClientOptions = {}) {
181
148
  this.options = {
@@ -184,130 +151,93 @@ export class HttpClient {
184
151
  this.baseUrl = this.options.baseUrl!;
185
152
  }
186
153
 
187
- private async doFetch(
154
+ protected async doFetch(
188
155
  path: string,
189
156
  options?: RequestInit
190
157
  ): Promise<Response> {
191
- if (this.options.stub) {
192
- return this.options.stub.containerFetch(path, options, this.options.port);
193
- }
194
- return fetch(this.baseUrl + path, options);
195
- }
196
- // Public methods to set event handlers
197
- setOnOutput(
198
- handler: (
199
- stream: "stdout" | "stderr",
200
- data: string,
201
- command: string
202
- ) => void
203
- ): void {
204
- this.options.onOutput = handler;
205
- }
206
-
207
- setOnCommandComplete(
208
- handler: (
209
- success: boolean,
210
- exitCode: number,
211
- stdout: string,
212
- stderr: string,
213
- command: string,
214
- args: string[]
215
- ) => void
216
- ): void {
217
- this.options.onCommandComplete = handler;
218
- }
219
-
220
- setOnStreamEvent(handler: (event: StreamEvent) => void): void {
221
- this.options.onStreamEvent = handler;
222
- }
158
+ const url = this.options.stub
159
+ ? `http://localhost:${this.options.port}${path}`
160
+ : `${this.baseUrl}${path}`;
161
+ const method = options?.method || "GET";
223
162
 
224
- // Public getter methods
225
- getOnOutput():
226
- | ((stream: "stdout" | "stderr", data: string, command: string) => void)
227
- | undefined {
228
- return this.options.onOutput;
229
- }
163
+ console.log(`[HTTP Client] Making ${method} request to ${url}`);
230
164
 
231
- getOnCommandComplete():
232
- | ((
233
- success: boolean,
234
- exitCode: number,
235
- stdout: string,
236
- stderr: string,
237
- command: string,
238
- args: string[]
239
- ) => void)
240
- | undefined {
241
- return this.options.onCommandComplete;
242
- }
165
+ try {
166
+ let response: Response;
243
167
 
244
- getOnStreamEvent(): ((event: StreamEvent) => void) | undefined {
245
- return this.options.onStreamEvent;
246
- }
168
+ if (this.options.stub) {
169
+ response = await this.options.stub.containerFetch(
170
+ url,
171
+ options,
172
+ this.options.port
173
+ );
174
+ } else {
175
+ response = await fetch(url, options);
176
+ }
247
177
 
248
- async createSession(): Promise<string> {
249
- try {
250
- const response = await this.doFetch(`/api/session/create`, {
251
- headers: {
252
- "Content-Type": "application/json",
253
- },
254
- method: "POST",
255
- });
178
+ console.log(
179
+ `[HTTP Client] Response: ${response.status} ${response.statusText}`
180
+ );
256
181
 
257
182
  if (!response.ok) {
258
- throw new Error(`HTTP error! status: ${response.status}`);
183
+ console.error(
184
+ `[HTTP Client] Request failed: ${method} ${url} - ${response.status} ${response.statusText}`
185
+ );
259
186
  }
260
187
 
261
- const data: SessionResponse = await response.json();
262
- this.sessionId = data.sessionId;
263
- console.log(`[HTTP Client] Created session: ${this.sessionId}`);
264
- return this.sessionId;
188
+ return response;
265
189
  } catch (error) {
266
- console.error("[HTTP Client] Error creating session:", error);
190
+ console.error(`[HTTP Client] Request error: ${method} ${url}`, error);
267
191
  throw error;
268
192
  }
269
193
  }
270
194
 
271
- async listSessions(): Promise<SessionListResponse> {
195
+ async createSession(options: {
196
+ id: string;
197
+ env?: Record<string, string>;
198
+ cwd?: string;
199
+ isolation?: boolean;
200
+ }): Promise<{ success: boolean; id: string; message: string }> {
272
201
  try {
273
- const response = await this.doFetch(`/api/session/list`, {
202
+ const response = await this.doFetch(`/api/session/create`, {
203
+ method: "POST",
274
204
  headers: {
275
205
  "Content-Type": "application/json",
276
206
  },
277
- method: "GET",
207
+ body: JSON.stringify(options),
278
208
  });
279
209
 
280
210
  if (!response.ok) {
281
- throw new Error(`HTTP error! status: ${response.status}`);
211
+ const errorData = (await response.json().catch(() => ({}))) as {
212
+ error?: string;
213
+ };
214
+ throw new Error(
215
+ errorData.error || `Failed to create session: ${response.status}`
216
+ );
282
217
  }
283
218
 
284
- const data: SessionListResponse = await response.json();
285
- console.log(`[HTTP Client] Listed ${data.count} sessions`);
219
+ const data = await response.json() as { success: boolean; id: string; message: string };
220
+ console.log(`[HTTP Client] Session created: ${options.id}`);
286
221
  return data;
287
222
  } catch (error) {
288
- console.error("[HTTP Client] Error listing sessions:", error);
223
+ console.error("[HTTP Client] Error creating session:", error);
289
224
  throw error;
290
225
  }
291
226
  }
292
227
 
293
- async execute(
228
+ async exec(
229
+ sessionId: string,
294
230
  command: string,
295
- args: string[] = [],
296
- sessionId?: string
231
+ options?: Pick<BaseExecOptions, "cwd" | "env">
297
232
  ): Promise<ExecuteResponse> {
298
233
  try {
299
- const targetSessionId = sessionId || this.sessionId;
300
-
234
+ // Always use session-specific endpoint
301
235
  const response = await this.doFetch(`/api/execute`, {
302
- body: JSON.stringify({
303
- args,
304
- command,
305
- sessionId: targetSessionId,
306
- }),
236
+ method: "POST",
307
237
  headers: {
308
238
  "Content-Type": "application/json",
309
239
  },
310
- method: "POST",
240
+ body: JSON.stringify({ id: sessionId, command }),
311
241
  });
312
242
 
313
243
  if (!response.ok) {
@@ -315,55 +245,57 @@ export class HttpClient {
315
245
  error?: string;
316
246
  };
317
247
  throw new Error(
318
- errorData.error || `HTTP error! status: ${response.status}`
248
+ errorData.error || `Failed to execute in session: ${response.status}`
319
249
  );
320
250
  }
321
251
 
322
- const data: ExecuteResponse = await response.json();
252
+ const data = await response.json() as { stdout: string; stderr: string; exitCode: number; success: boolean };
323
253
  console.log(
324
- `[HTTP Client] Command executed: ${command}, Success: ${data.success}`
254
+ `[HTTP Client] Command executed in session ${sessionId}: ${command}`
325
255
  );
256
+
257
+ // Convert to ExecuteResponse format for consistency
258
+ const executeResponse: ExecuteResponse = {
259
+ ...data,
260
+ command,
261
+ timestamp: new Date().toISOString()
262
+ };
326
263
 
327
264
  // Call the callback if provided
328
265
  this.options.onCommandComplete?.(
329
- data.success,
330
- data.exitCode,
331
- data.stdout,
332
- data.stderr,
333
- data.command,
334
- data.args
266
+ executeResponse.success,
267
+ executeResponse.exitCode,
268
+ executeResponse.stdout,
269
+ executeResponse.stderr,
270
+ executeResponse.command
335
271
  );
336
272
 
337
- return data;
273
+ return executeResponse;
338
274
  } catch (error) {
339
- console.error("[HTTP Client] Error executing command:", error);
275
+ console.error("[HTTP Client] Error executing in session:", error);
340
276
  this.options.onError?.(
341
277
  error instanceof Error ? error.message : "Unknown error",
342
- command,
343
- args
278
+ command
344
279
  );
345
280
  throw error;
346
281
  }
347
282
  }
348
283
 
349
- async executeStream(
350
- command: string,
351
- args: string[] = [],
352
- sessionId?: string
353
- ): Promise<void> {
284
+ async execStream(
285
+ sessionId: string,
286
+ command: string
287
+ ): Promise<ReadableStream<Uint8Array>> {
354
288
  try {
355
- const targetSessionId = sessionId || this.sessionId;
356
-
289
+ // Always use session-specific streaming endpoint
357
290
  const response = await this.doFetch(`/api/execute/stream`, {
358
- body: JSON.stringify({
359
- args,
360
- command,
361
- sessionId: targetSessionId,
362
- }),
291
+ method: "POST",
363
292
  headers: {
364
293
  "Content-Type": "application/json",
365
294
  },
366
- method: "POST",
295
+ body: JSON.stringify({
296
+ id: sessionId,
297
+ command
298
+ }),
367
299
  });
368
300
 
369
301
  if (!response.ok) {
@@ -371,123 +303,38 @@ export class HttpClient {
371
303
  error?: string;
372
304
  };
373
305
  throw new Error(
374
- errorData.error || `HTTP error! status: ${response.status}`
306
+ errorData.error || `Failed to stream execute in session: ${response.status}`
375
307
  );
376
308
  }
377
309
 
378
310
  if (!response.body) {
379
- throw new Error("No response body for streaming request");
311
+ throw new Error("No response body for streaming execution");
380
312
  }
381
313
 
382
- const reader = response.body.getReader();
383
- const decoder = new TextDecoder();
384
-
385
- try {
386
- while (true) {
387
- const { done, value } = await reader.read();
388
-
389
- if (done) {
390
- break;
391
- }
392
-
393
- const chunk = decoder.decode(value, { stream: true });
394
- const lines = chunk.split("\n");
395
-
396
- for (const line of lines) {
397
- if (line.startsWith("data: ")) {
398
- try {
399
- const eventData = line.slice(6); // Remove 'data: ' prefix
400
- const event: StreamEvent = JSON.parse(eventData);
401
-
402
- console.log(`[HTTP Client] Stream event: ${event.type}`);
403
- this.options.onStreamEvent?.(event);
404
-
405
- switch (event.type) {
406
- case "command_start":
407
- console.log(
408
- `[HTTP Client] Command started: ${
409
- event.command
410
- } ${event.args?.join(" ")}`
411
- );
412
- this.options.onCommandStart?.(
413
- event.command!,
414
- event.args || []
415
- );
416
- break;
417
-
418
- case "output":
419
- console.log(`[${event.stream}] ${event.data}`);
420
- this.options.onOutput?.(
421
- event.stream!,
422
- event.data!,
423
- event.command!
424
- );
425
- break;
426
-
427
- case "command_complete":
428
- console.log(
429
- `[HTTP Client] Command completed: ${event.command}, Success: ${event.success}, Exit code: ${event.exitCode}`
430
- );
431
- this.options.onCommandComplete?.(
432
- event.success!,
433
- event.exitCode!,
434
- event.stdout!,
435
- event.stderr!,
436
- event.command!,
437
- event.args || []
438
- );
439
- break;
440
-
441
- case "error":
442
- console.error(
443
- `[HTTP Client] Command error: ${event.error}`
444
- );
445
- this.options.onError?.(
446
- event.error!,
447
- event.command,
448
- event.args
449
- );
450
- break;
451
- }
452
- } catch (parseError) {
453
- console.warn(
454
- "[HTTP Client] Failed to parse stream event:",
455
- parseError
456
- );
457
- }
458
- }
459
- }
460
- }
461
- } finally {
462
- reader.releaseLock();
463
- }
464
- } catch (error) {
465
- console.error("[HTTP Client] Error in streaming execution:", error);
466
- this.options.onError?.(
467
- error instanceof Error ? error.message : "Unknown error",
468
- command,
469
- args
314
+ console.log(
315
+ `[HTTP Client] Started streaming command in session ${sessionId}: ${command}`
470
316
  );
317
+ return response.body;
318
+ } catch (error) {
319
+ console.error("[HTTP Client] Error streaming execute in session:", error);
471
320
  throw error;
472
321
  }
473
322
  }
474
323
 
475
324
  async gitCheckout(
476
325
  repoUrl: string,
326
+ sessionId: string,
477
327
  branch: string = "main",
478
- targetDir?: string,
479
- sessionId?: string
328
+ targetDir?: string
480
329
  ): Promise<GitCheckoutResponse> {
481
330
  try {
482
- const targetSessionId = sessionId || this.sessionId;
483
-
484
331
  const response = await this.doFetch(`/api/git/checkout`, {
485
332
  body: JSON.stringify({
486
333
  branch,
487
334
  repoUrl,
488
- sessionId: targetSessionId,
489
335
  targetDir,
490
- }),
336
+ sessionId,
337
+ } as GitCheckoutRequest),
491
338
  headers: {
492
339
  "Content-Type": "application/json",
493
340
  },
@@ -515,150 +362,18 @@ export class HttpClient {
515
362
  }
516
363
  }
517
364
 
518
- async gitCheckoutStream(
519
- repoUrl: string,
520
- branch: string = "main",
521
- targetDir?: string,
522
- sessionId?: string
523
- ): Promise<void> {
524
- try {
525
- const targetSessionId = sessionId || this.sessionId;
526
-
527
- const response = await this.doFetch(`/api/git/checkout/stream`, {
528
- body: JSON.stringify({
529
- branch,
530
- repoUrl,
531
- sessionId: targetSessionId,
532
- targetDir,
533
- }),
534
- headers: {
535
- "Content-Type": "application/json",
536
- },
537
- method: "POST",
538
- });
539
-
540
- if (!response.ok) {
541
- const errorData = (await response.json().catch(() => ({}))) as {
542
- error?: string;
543
- };
544
- throw new Error(
545
- errorData.error || `HTTP error! status: ${response.status}`
546
- );
547
- }
548
-
549
- if (!response.body) {
550
- throw new Error("No response body for streaming request");
551
- }
552
-
553
- const reader = response.body.getReader();
554
- const decoder = new TextDecoder();
555
-
556
- try {
557
- while (true) {
558
- const { done, value } = await reader.read();
559
-
560
- if (done) {
561
- break;
562
- }
563
-
564
- const chunk = decoder.decode(value, { stream: true });
565
- const lines = chunk.split("\n");
566
-
567
- for (const line of lines) {
568
- if (line.startsWith("data: ")) {
569
- try {
570
- const eventData = line.slice(6); // Remove 'data: ' prefix
571
- const event: StreamEvent = JSON.parse(eventData);
572
-
573
- console.log(
574
- `[HTTP Client] Git checkout stream event: ${event.type}`
575
- );
576
- this.options.onStreamEvent?.(event);
577
-
578
- switch (event.type) {
579
- case "command_start":
580
- console.log(
581
- `[HTTP Client] Git checkout started: ${
582
- event.command
583
- } ${event.args?.join(" ")}`
584
- );
585
- this.options.onCommandStart?.(
586
- event.command!,
587
- event.args || []
588
- );
589
- break;
590
-
591
- case "output":
592
- console.log(`[${event.stream}] ${event.data}`);
593
- this.options.onOutput?.(
594
- event.stream!,
595
- event.data!,
596
- event.command!
597
- );
598
- break;
599
-
600
- case "command_complete":
601
- console.log(
602
- `[HTTP Client] Git checkout completed: ${event.command}, Success: ${event.success}, Exit code: ${event.exitCode}`
603
- );
604
- this.options.onCommandComplete?.(
605
- event.success!,
606
- event.exitCode!,
607
- event.stdout!,
608
- event.stderr!,
609
- event.command!,
610
- event.args || []
611
- );
612
- break;
613
-
614
- case "error":
615
- console.error(
616
- `[HTTP Client] Git checkout error: ${event.error}`
617
- );
618
- this.options.onError?.(
619
- event.error!,
620
- event.command,
621
- event.args
622
- );
623
- break;
624
- }
625
- } catch (parseError) {
626
- console.warn(
627
- "[HTTP Client] Failed to parse git checkout stream event:",
628
- parseError
629
- );
630
- }
631
- }
632
- }
633
- }
634
- } finally {
635
- reader.releaseLock();
636
- }
637
- } catch (error) {
638
- console.error("[HTTP Client] Error in streaming git checkout:", error);
639
- this.options.onError?.(
640
- error instanceof Error ? error.message : "Unknown error",
641
- "git clone",
642
- [branch, repoUrl, targetDir || ""]
643
- );
644
- throw error;
645
- }
646
- }
647
-
648
365
  async mkdir(
649
366
  path: string,
650
367
  recursive: boolean = false,
651
- sessionId?: string
368
+ sessionId: string
652
369
  ): Promise<MkdirResponse> {
653
370
  try {
654
- const targetSessionId = sessionId || this.sessionId;
655
-
656
371
  const response = await this.doFetch(`/api/mkdir`, {
657
372
  body: JSON.stringify({
658
373
  path,
659
374
  recursive,
660
- sessionId: targetSessionId,
661
- }),
375
+ sessionId,
376
+ } as MkdirRequest),
662
377
  headers: {
663
378
  "Content-Type": "application/json",
664
379
  },
@@ -676,7 +391,7 @@ export class HttpClient {
676
391
 
677
392
  const data: MkdirResponse = await response.json();
678
393
  console.log(
679
- `[HTTP Client] Directory created: ${path}, Success: ${data.success}, Recursive: ${data.recursive}`
394
+ `[HTTP Client] Directory created: ${path}, Success: ${data.success}, Recursive: ${data.recursive}${sessionId ? ` in session: ${sessionId}` : ''}`
680
395
  );
681
396
 
682
397
  return data;
@@ -686,20 +401,20 @@ export class HttpClient {
686
401
  }
687
402
  }
688
403
 
689
- async mkdirStream(
404
+ async writeFile(
690
405
  path: string,
691
- recursive: boolean = false,
692
- sessionId?: string
693
- ): Promise<void> {
406
+ content: string,
407
+ encoding: string = "utf-8",
408
+ sessionId: string
409
+ ): Promise<WriteFileResponse> {
694
410
  try {
695
- const targetSessionId = sessionId || this.sessionId;
696
-
697
- const response = await this.doFetch(`/api/mkdir/stream`, {
411
+ const response = await this.doFetch(`/api/write`, {
698
412
  body: JSON.stringify({
413
+ content,
414
+ encoding,
699
415
  path,
700
- recursive,
701
- sessionId: targetSessionId,
702
- }),
416
+ sessionId,
417
+ } as WriteFileRequest),
703
418
  headers: {
704
419
  "Content-Type": "application/json",
705
420
  },
@@ -715,117 +430,30 @@ export class HttpClient {
715
430
  );
716
431
  }
717
432
 
718
- if (!response.body) {
719
- throw new Error("No response body for streaming request");
720
- }
433
+ const data: WriteFileResponse = await response.json();
434
+ console.log(
435
+ `[HTTP Client] File written: ${path}, Success: ${data.success}${sessionId ? ` in session: ${sessionId}` : ''}`
436
+ );
721
437
 
722
- const reader = response.body.getReader();
723
- const decoder = new TextDecoder();
724
-
725
- try {
726
- while (true) {
727
- const { done, value } = await reader.read();
728
-
729
- if (done) {
730
- break;
731
- }
732
-
733
- const chunk = decoder.decode(value, { stream: true });
734
- const lines = chunk.split("\n");
735
-
736
- for (const line of lines) {
737
- if (line.startsWith("data: ")) {
738
- try {
739
- const eventData = line.slice(6); // Remove 'data: ' prefix
740
- const event: StreamEvent = JSON.parse(eventData);
741
-
742
- console.log(`[HTTP Client] Mkdir stream event: ${event.type}`);
743
- this.options.onStreamEvent?.(event);
744
-
745
- switch (event.type) {
746
- case "command_start":
747
- console.log(
748
- `[HTTP Client] Mkdir started: ${
749
- event.command
750
- } ${event.args?.join(" ")}`
751
- );
752
- this.options.onCommandStart?.(
753
- event.command!,
754
- event.args || []
755
- );
756
- break;
757
-
758
- case "output":
759
- console.log(`[${event.stream}] ${event.data}`);
760
- this.options.onOutput?.(
761
- event.stream!,
762
- event.data!,
763
- event.command!
764
- );
765
- break;
766
-
767
- case "command_complete":
768
- console.log(
769
- `[HTTP Client] Mkdir completed: ${event.command}, Success: ${event.success}, Exit code: ${event.exitCode}`
770
- );
771
- this.options.onCommandComplete?.(
772
- event.success!,
773
- event.exitCode!,
774
- event.stdout!,
775
- event.stderr!,
776
- event.command!,
777
- event.args || []
778
- );
779
- break;
780
-
781
- case "error":
782
- console.error(`[HTTP Client] Mkdir error: ${event.error}`);
783
- this.options.onError?.(
784
- event.error!,
785
- event.command,
786
- event.args
787
- );
788
- break;
789
- }
790
- } catch (parseError) {
791
- console.warn(
792
- "[HTTP Client] Failed to parse mkdir stream event:",
793
- parseError
794
- );
795
- }
796
- }
797
- }
798
- }
799
- } finally {
800
- reader.releaseLock();
801
- }
438
+ return data;
802
439
  } catch (error) {
803
- console.error("[HTTP Client] Error in streaming mkdir:", error);
804
- this.options.onError?.(
805
- error instanceof Error ? error.message : "Unknown error",
806
- "mkdir",
807
- recursive ? ["-p", path] : [path]
808
- );
440
+ console.error("[HTTP Client] Error writing file:", error);
809
441
  throw error;
810
442
  }
811
443
  }
812
444
 
813
- async writeFile(
445
+ async readFile(
814
446
  path: string,
815
- content: string,
816
447
  encoding: string = "utf-8",
817
- sessionId?: string
818
- ): Promise<WriteFileResponse> {
448
+ sessionId: string
449
+ ): Promise<ReadFileResponse> {
819
450
  try {
820
- const targetSessionId = sessionId || this.sessionId;
821
-
822
- const response = await this.doFetch(`/api/write`, {
451
+ const response = await this.doFetch(`/api/read`, {
823
452
  body: JSON.stringify({
824
- content,
825
453
  encoding,
826
454
  path,
827
- sessionId: targetSessionId,
828
- }),
455
+ sessionId,
456
+ } as ReadFileRequest),
829
457
  headers: {
830
458
  "Content-Type": "application/json",
831
459
  },
@@ -841,38 +469,32 @@ export class HttpClient {
841
469
  );
842
470
  }
843
471
 
844
- const data: WriteFileResponse = await response.json();
472
+ const data: ReadFileResponse = await response.json();
845
473
  console.log(
846
- `[HTTP Client] File written: ${path}, Success: ${data.success}`
474
+ `[HTTP Client] File read: ${path}, Success: ${data.success}, Content length: ${data.content.length}${sessionId ? ` in session: ${sessionId}` : ''}`
847
475
  );
848
476
 
849
477
  return data;
850
478
  } catch (error) {
851
- console.error("[HTTP Client] Error writing file:", error);
479
+ console.error("[HTTP Client] Error reading file:", error);
852
480
  throw error;
853
481
  }
854
482
  }
855
483
 
856
- async writeFileStream(
484
+ async readFileStream(
857
485
  path: string,
858
- content: string,
859
- encoding: string = "utf-8",
860
- sessionId?: string
861
- ): Promise<void> {
486
+ sessionId: string
487
+ ): Promise<ReadableStream<Uint8Array>> {
862
488
  try {
863
- const targetSessionId = sessionId || this.sessionId;
864
-
865
- const response = await this.doFetch(`/api/write/stream`, {
866
- body: JSON.stringify({
867
- content,
868
- encoding,
869
- path,
870
- sessionId: targetSessionId,
871
- }),
489
+ const response = await this.doFetch(`/api/read/stream`, {
490
+ method: "POST",
872
491
  headers: {
873
492
  "Content-Type": "application/json",
874
493
  },
875
- method: "POST",
494
+ body: JSON.stringify({
495
+ path,
496
+ sessionId,
497
+ } as ReadFileRequest),
876
498
  });
877
499
 
878
500
  if (!response.ok) {
@@ -885,111 +507,29 @@ export class HttpClient {
885
507
  }
886
508
 
887
509
  if (!response.body) {
888
- throw new Error("No response body for streaming request");
510
+ throw new Error("No response body for file streaming");
889
511
  }
890
512
 
891
- const reader = response.body.getReader();
892
- const decoder = new TextDecoder();
893
-
894
- try {
895
- while (true) {
896
- const { done, value } = await reader.read();
897
-
898
- if (done) {
899
- break;
900
- }
901
-
902
- const chunk = decoder.decode(value, { stream: true });
903
- const lines = chunk.split("\n");
904
-
905
- for (const line of lines) {
906
- if (line.startsWith("data: ")) {
907
- try {
908
- const eventData = line.slice(6); // Remove 'data: ' prefix
909
- const event: StreamEvent = JSON.parse(eventData);
910
-
911
- console.log(
912
- `[HTTP Client] Write file stream event: ${event.type}`
913
- );
914
- this.options.onStreamEvent?.(event);
915
-
916
- switch (event.type) {
917
- case "command_start":
918
- console.log(
919
- `[HTTP Client] Write file started: ${event.path}`
920
- );
921
- this.options.onCommandStart?.("write", [
922
- path,
923
- content,
924
- encoding,
925
- ]);
926
- break;
927
-
928
- case "output":
929
- console.log(`[output] ${event.message}`);
930
- this.options.onOutput?.("stdout", event.message!, "write");
931
- break;
932
-
933
- case "command_complete":
934
- console.log(
935
- `[HTTP Client] Write file completed: ${event.path}, Success: ${event.success}`
936
- );
937
- this.options.onCommandComplete?.(
938
- event.success!,
939
- 0,
940
- "",
941
- "",
942
- "write",
943
- [path, content, encoding]
944
- );
945
- break;
946
-
947
- case "error":
948
- console.error(
949
- `[HTTP Client] Write file error: ${event.error}`
950
- );
951
- this.options.onError?.(event.error!, "write", [
952
- path,
953
- content,
954
- encoding,
955
- ]);
956
- break;
957
- }
958
- } catch (parseError) {
959
- console.warn(
960
- "[HTTP Client] Failed to parse write file stream event:",
961
- parseError
962
- );
963
- }
964
- }
965
- }
966
- }
967
- } finally {
968
- reader.releaseLock();
969
- }
970
- } catch (error) {
971
- console.error("[HTTP Client] Error in streaming write file:", error);
972
- this.options.onError?.(
973
- error instanceof Error ? error.message : "Unknown error",
974
- "write",
975
- [path, content, encoding]
513
+ console.log(
514
+ `[HTTP Client] Started streaming file: ${path}${sessionId ? ` in session: ${sessionId}` : ''}`
976
515
  );
516
+ return response.body;
517
+ } catch (error) {
518
+ console.error("[HTTP Client] Error streaming file:", error);
977
519
  throw error;
978
520
  }
979
521
  }
980
522
 
981
523
  async deleteFile(
982
524
  path: string,
983
- sessionId?: string
525
+ sessionId: string
984
526
  ): Promise<DeleteFileResponse> {
985
527
  try {
986
- const targetSessionId = sessionId || this.sessionId;
987
-
988
528
  const response = await this.doFetch(`/api/delete`, {
989
529
  body: JSON.stringify({
990
530
  path,
991
- sessionId: targetSessionId,
992
- }),
531
+ sessionId,
532
+ } as DeleteFileRequest),
993
533
  headers: {
994
534
  "Content-Type": "application/json",
995
535
  },
@@ -1007,7 +547,7 @@ export class HttpClient {
1007
547
 
1008
548
  const data: DeleteFileResponse = await response.json();
1009
549
  console.log(
1010
- `[HTTP Client] File deleted: ${path}, Success: ${data.success}`
550
+ `[HTTP Client] File deleted: ${path}, Success: ${data.success}${sessionId ? ` in session: ${sessionId}` : ''}`
1011
551
  );
1012
552
 
1013
553
  return data;
@@ -1017,15 +557,18 @@ export class HttpClient {
1017
557
  }
1018
558
  }
1019
559
 
1020
- async deleteFileStream(path: string, sessionId?: string): Promise<void> {
560
+ async renameFile(
561
+ oldPath: string,
562
+ newPath: string,
563
+ sessionId: string
564
+ ): Promise<RenameFileResponse> {
1021
565
  try {
1022
- const targetSessionId = sessionId || this.sessionId;
1023
-
1024
- const response = await this.doFetch(`/api/delete/stream`, {
566
+ const response = await this.doFetch(`/api/rename`, {
1025
567
  body: JSON.stringify({
1026
- path,
1027
- sessionId: targetSessionId,
1028
- }),
568
+ newPath,
569
+ oldPath,
570
+ sessionId,
571
+ } as RenameFileRequest),
1029
572
  headers: {
1030
573
  "Content-Type": "application/json",
1031
574
  },
@@ -1041,101 +584,30 @@ export class HttpClient {
1041
584
  );
1042
585
  }
1043
586
 
1044
- if (!response.body) {
1045
- throw new Error("No response body for streaming request");
1046
- }
587
+ const data: RenameFileResponse = await response.json();
588
+ console.log(
589
+ `[HTTP Client] File renamed: ${oldPath} -> ${newPath}, Success: ${data.success}${sessionId ? ` in session: ${sessionId}` : ''}`
590
+ );
1047
591
 
1048
- const reader = response.body.getReader();
1049
- const decoder = new TextDecoder();
1050
-
1051
- try {
1052
- while (true) {
1053
- const { done, value } = await reader.read();
1054
-
1055
- if (done) {
1056
- break;
1057
- }
1058
-
1059
- const chunk = decoder.decode(value, { stream: true });
1060
- const lines = chunk.split("\n");
1061
-
1062
- for (const line of lines) {
1063
- if (line.startsWith("data: ")) {
1064
- try {
1065
- const eventData = line.slice(6); // Remove 'data: ' prefix
1066
- const event: StreamEvent = JSON.parse(eventData);
1067
-
1068
- console.log(
1069
- `[HTTP Client] Delete file stream event: ${event.type}`
1070
- );
1071
- this.options.onStreamEvent?.(event);
1072
-
1073
- switch (event.type) {
1074
- case "command_start":
1075
- console.log(
1076
- `[HTTP Client] Delete file started: ${event.path}`
1077
- );
1078
- this.options.onCommandStart?.("delete", [path]);
1079
- break;
1080
-
1081
- case "command_complete":
1082
- console.log(
1083
- `[HTTP Client] Delete file completed: ${event.path}, Success: ${event.success}`
1084
- );
1085
- this.options.onCommandComplete?.(
1086
- event.success!,
1087
- 0,
1088
- "",
1089
- "",
1090
- "delete",
1091
- [path]
1092
- );
1093
- break;
1094
-
1095
- case "error":
1096
- console.error(
1097
- `[HTTP Client] Delete file error: ${event.error}`
1098
- );
1099
- this.options.onError?.(event.error!, "delete", [path]);
1100
- break;
1101
- }
1102
- } catch (parseError) {
1103
- console.warn(
1104
- "[HTTP Client] Failed to parse delete file stream event:",
1105
- parseError
1106
- );
1107
- }
1108
- }
1109
- }
1110
- }
1111
- } finally {
1112
- reader.releaseLock();
1113
- }
592
+ return data;
1114
593
  } catch (error) {
1115
- console.error("[HTTP Client] Error in streaming delete file:", error);
1116
- this.options.onError?.(
1117
- error instanceof Error ? error.message : "Unknown error",
1118
- "delete",
1119
- [path]
1120
- );
594
+ console.error("[HTTP Client] Error renaming file:", error);
1121
595
  throw error;
1122
596
  }
1123
597
  }
1124
598
 
1125
- async renameFile(
1126
- oldPath: string,
1127
- newPath: string,
1128
- sessionId?: string
1129
- ): Promise<RenameFileResponse> {
599
+ async moveFile(
600
+ sourcePath: string,
601
+ destinationPath: string,
602
+ sessionId: string
603
+ ): Promise<MoveFileResponse> {
1130
604
  try {
1131
- const targetSessionId = sessionId || this.sessionId;
1132
-
1133
- const response = await this.doFetch(`/api/rename`, {
605
+ const response = await this.doFetch(`/api/move`, {
1134
606
  body: JSON.stringify({
1135
- newPath,
1136
- oldPath,
1137
- sessionId: targetSessionId,
1138
- }),
607
+ destinationPath,
608
+ sourcePath,
609
+ sessionId,
610
+ } as MoveFileRequest),
1139
611
  headers: {
1140
612
  "Content-Type": "application/json",
1141
613
  },
@@ -1151,32 +623,33 @@ export class HttpClient {
1151
623
  );
1152
624
  }
1153
625
 
1154
- const data: RenameFileResponse = await response.json();
626
+ const data: MoveFileResponse = await response.json();
1155
627
  console.log(
1156
- `[HTTP Client] File renamed: ${oldPath} -> ${newPath}, Success: ${data.success}`
628
+ `[HTTP Client] File moved: ${sourcePath} -> ${destinationPath}, Success: ${data.success}${sessionId ? ` in session: ${sessionId}` : ''}`
1157
629
  );
1158
630
 
1159
631
  return data;
1160
632
  } catch (error) {
1161
- console.error("[HTTP Client] Error renaming file:", error);
633
+ console.error("[HTTP Client] Error moving file:", error);
1162
634
  throw error;
1163
635
  }
1164
636
  }
1165
637
 
1166
- async renameFileStream(
1167
- oldPath: string,
1168
- newPath: string,
1169
- sessionId?: string
1170
- ): Promise<void> {
638
+ async listFiles(
639
+ path: string,
640
+ sessionId: string,
641
+ options?: {
642
+ recursive?: boolean;
643
+ includeHidden?: boolean;
644
+ }
645
+ ): Promise<ListFilesResponse> {
1171
646
  try {
1172
- const targetSessionId = sessionId || this.sessionId;
1173
-
1174
- const response = await this.doFetch(`/api/rename/stream`, {
647
+ const response = await this.doFetch(`/api/list-files`, {
1175
648
  body: JSON.stringify({
1176
- newPath,
1177
- oldPath,
1178
- sessionId: targetSessionId,
1179
- }),
649
+ path,
650
+ options,
651
+ sessionId,
652
+ } as ListFilesRequest),
1180
653
  headers: {
1181
654
  "Content-Type": "application/json",
1182
655
  },
@@ -1192,103 +665,24 @@ export class HttpClient {
1192
665
  );
1193
666
  }
1194
667
 
1195
- if (!response.body) {
1196
- throw new Error("No response body for streaming request");
1197
- }
668
+ const data: ListFilesResponse = await response.json();
669
+ console.log(
670
+ `[HTTP Client] Listed ${data.files.length} files in: ${path}, Success: ${data.success}${sessionId ? ` in session: ${sessionId}` : ''}`
671
+ );
1198
672
 
1199
- const reader = response.body.getReader();
1200
- const decoder = new TextDecoder();
1201
-
1202
- try {
1203
- while (true) {
1204
- const { done, value } = await reader.read();
1205
-
1206
- if (done) {
1207
- break;
1208
- }
1209
-
1210
- const chunk = decoder.decode(value, { stream: true });
1211
- const lines = chunk.split("\n");
1212
-
1213
- for (const line of lines) {
1214
- if (line.startsWith("data: ")) {
1215
- try {
1216
- const eventData = line.slice(6); // Remove 'data: ' prefix
1217
- const event: StreamEvent = JSON.parse(eventData);
1218
-
1219
- console.log(
1220
- `[HTTP Client] Rename file stream event: ${event.type}`
1221
- );
1222
- this.options.onStreamEvent?.(event);
1223
-
1224
- switch (event.type) {
1225
- case "command_start":
1226
- console.log(
1227
- `[HTTP Client] Rename file started: ${event.oldPath} -> ${event.newPath}`
1228
- );
1229
- this.options.onCommandStart?.("rename", [oldPath, newPath]);
1230
- break;
1231
-
1232
- case "command_complete":
1233
- console.log(
1234
- `[HTTP Client] Rename file completed: ${event.oldPath} -> ${event.newPath}, Success: ${event.success}`
1235
- );
1236
- this.options.onCommandComplete?.(
1237
- event.success!,
1238
- 0,
1239
- "",
1240
- "",
1241
- "rename",
1242
- [oldPath, newPath]
1243
- );
1244
- break;
1245
-
1246
- case "error":
1247
- console.error(
1248
- `[HTTP Client] Rename file error: ${event.error}`
1249
- );
1250
- this.options.onError?.(event.error!, "rename", [
1251
- oldPath,
1252
- newPath,
1253
- ]);
1254
- break;
1255
- }
1256
- } catch (parseError) {
1257
- console.warn(
1258
- "[HTTP Client] Failed to parse rename file stream event:",
1259
- parseError
1260
- );
1261
- }
1262
- }
1263
- }
1264
- }
1265
- } finally {
1266
- reader.releaseLock();
1267
- }
673
+ return data;
1268
674
  } catch (error) {
1269
- console.error("[HTTP Client] Error in streaming rename file:", error);
1270
- this.options.onError?.(
1271
- error instanceof Error ? error.message : "Unknown error",
1272
- "rename",
1273
- [oldPath, newPath]
1274
- );
675
+ console.error("[HTTP Client] Error listing files:", error);
1275
676
  throw error;
1276
677
  }
1277
678
  }
1278
679
 
1279
- async moveFile(
1280
- sourcePath: string,
1281
- destinationPath: string,
1282
- sessionId?: string
1283
- ): Promise<MoveFileResponse> {
680
+ async exposePort(port: number, name?: string): Promise<ExposePortResponse> {
1284
681
  try {
1285
- const targetSessionId = sessionId || this.sessionId;
1286
-
1287
- const response = await this.doFetch(`/api/move`, {
682
+ const response = await this.doFetch(`/api/expose-port`, {
1288
683
  body: JSON.stringify({
1289
- destinationPath,
1290
- sessionId: targetSessionId,
1291
- sourcePath,
684
+ port,
685
+ name,
1292
686
  }),
1293
687
  headers: {
1294
688
  "Content-Type": "application/json",
@@ -1300,41 +694,36 @@ export class HttpClient {
1300
694
  const errorData = (await response.json().catch(() => ({}))) as {
1301
695
  error?: string;
1302
696
  };
697
+ console.log(errorData);
1303
698
  throw new Error(
1304
699
  errorData.error || `HTTP error! status: ${response.status}`
1305
700
  );
1306
701
  }
1307
702
 
1308
- const data: MoveFileResponse = await response.json();
703
+ const data: ExposePortResponse = await response.json();
1309
704
  console.log(
1310
- `[HTTP Client] File moved: ${sourcePath} -> ${destinationPath}, Success: ${data.success}`
705
+ `[HTTP Client] Port exposed: ${port}${
706
+ name ? ` (${name})` : ""
707
+ }, Success: ${data.success}`
1311
708
  );
1312
709
 
1313
710
  return data;
1314
711
  } catch (error) {
1315
- console.error("[HTTP Client] Error moving file:", error);
712
+ console.error("[HTTP Client] Error exposing port:", error);
1316
713
  throw error;
1317
714
  }
1318
715
  }
1319
716
 
1320
- async moveFileStream(
1321
- sourcePath: string,
1322
- destinationPath: string,
1323
- sessionId?: string
1324
- ): Promise<void> {
717
+ async unexposePort(port: number): Promise<UnexposePortResponse> {
1325
718
  try {
1326
- const targetSessionId = sessionId || this.sessionId;
1327
-
1328
- const response = await this.doFetch(`/api/move/stream`, {
719
+ const response = await this.doFetch(`/api/unexpose-port`, {
1329
720
  body: JSON.stringify({
1330
- destinationPath,
1331
- sessionId: targetSessionId,
1332
- sourcePath,
721
+ port,
1333
722
  }),
1334
723
  headers: {
1335
724
  "Content-Type": "application/json",
1336
725
  },
1337
- method: "POST",
726
+ method: "DELETE",
1338
727
  });
1339
728
 
1340
729
  if (!response.ok) {
@@ -1346,89 +735,42 @@ export class HttpClient {
1346
735
  );
1347
736
  }
1348
737
 
1349
- if (!response.body) {
1350
- throw new Error("No response body for streaming request");
1351
- }
738
+ const data: UnexposePortResponse = await response.json();
739
+ console.log(
740
+ `[HTTP Client] Port unexposed: ${port}, Success: ${data.success}`
741
+ );
1352
742
 
1353
- const reader = response.body.getReader();
1354
- const decoder = new TextDecoder();
1355
-
1356
- try {
1357
- while (true) {
1358
- const { done, value } = await reader.read();
1359
-
1360
- if (done) {
1361
- break;
1362
- }
1363
-
1364
- const chunk = decoder.decode(value, { stream: true });
1365
- const lines = chunk.split("\n");
1366
-
1367
- for (const line of lines) {
1368
- if (line.startsWith("data: ")) {
1369
- try {
1370
- const eventData = line.slice(6); // Remove 'data: ' prefix
1371
- const event: StreamEvent = JSON.parse(eventData);
1372
-
1373
- console.log(
1374
- `[HTTP Client] Move file stream event: ${event.type}`
1375
- );
1376
- this.options.onStreamEvent?.(event);
1377
-
1378
- switch (event.type) {
1379
- case "command_start":
1380
- console.log(
1381
- `[HTTP Client] Move file started: ${event.sourcePath} -> ${event.destinationPath}`
1382
- );
1383
- this.options.onCommandStart?.("move", [
1384
- sourcePath,
1385
- destinationPath,
1386
- ]);
1387
- break;
1388
-
1389
- case "command_complete":
1390
- console.log(
1391
- `[HTTP Client] Move file completed: ${event.sourcePath} -> ${event.destinationPath}, Success: ${event.success}`
1392
- );
1393
- this.options.onCommandComplete?.(
1394
- event.success!,
1395
- 0,
1396
- "",
1397
- "",
1398
- "move",
1399
- [sourcePath, destinationPath]
1400
- );
1401
- break;
1402
-
1403
- case "error":
1404
- console.error(
1405
- `[HTTP Client] Move file error: ${event.error}`
1406
- );
1407
- this.options.onError?.(event.error!, "move", [
1408
- sourcePath,
1409
- destinationPath,
1410
- ]);
1411
- break;
1412
- }
1413
- } catch (parseError) {
1414
- console.warn(
1415
- "[HTTP Client] Failed to parse move file stream event:",
1416
- parseError
1417
- );
1418
- }
1419
- }
1420
- }
1421
- }
1422
- } finally {
1423
- reader.releaseLock();
743
+ return data;
744
+ } catch (error) {
745
+ console.error("[HTTP Client] Error unexposing port:", error);
746
+ throw error;
747
+ }
748
+ }
749
+
750
+ async getExposedPorts(): Promise<GetExposedPortsResponse> {
751
+ try {
752
+ const response = await this.doFetch(`/api/exposed-ports`, {
753
+ headers: {
754
+ "Content-Type": "application/json",
755
+ },
756
+ method: "GET",
757
+ });
758
+
759
+ if (!response.ok) {
760
+ const errorData = (await response.json().catch(() => ({}))) as {
761
+ error?: string;
762
+ };
763
+ throw new Error(
764
+ errorData.error || `HTTP error! status: ${response.status}`
765
+ );
1424
766
  }
767
+
768
+ const data: GetExposedPortsResponse = await response.json();
769
+ console.log(`[HTTP Client] Got ${data.count} exposed ports`);
770
+
771
+ return data;
1425
772
  } catch (error) {
1426
- console.error("[HTTP Client] Error in streaming move file:", error);
1427
- this.options.onError?.(
1428
- error instanceof Error ? error.message : "Unknown error",
1429
- "move",
1430
- [sourcePath, destinationPath]
1431
- );
773
+ console.error("[HTTP Client] Error getting exposed ports:", error);
1432
774
  throw error;
1433
775
  }
1434
776
  }
@@ -1455,270 +797,252 @@ export class HttpClient {
1455
797
  }
1456
798
  }
1457
799
 
1458
- async getCommands(): Promise<string[]> {
800
+
801
+ // Process management methods
802
+ async startProcess(
803
+ command: string,
804
+ sessionId: string,
805
+ options?: {
806
+ processId?: string;
807
+ timeout?: number;
808
+ env?: Record<string, string>;
809
+ cwd?: string;
810
+ encoding?: string;
811
+ autoCleanup?: boolean;
812
+ }
813
+ ): Promise<StartProcessResponse> {
1459
814
  try {
1460
- const response = await fetch(`${this.baseUrl}/api/commands`, {
815
+ const response = await this.doFetch("/api/process/start", {
816
+ body: JSON.stringify({
817
+ command,
818
+ sessionId,
819
+ options,
820
+ } as StartProcessRequest),
1461
821
  headers: {
1462
822
  "Content-Type": "application/json",
1463
823
  },
1464
- method: "GET",
824
+ method: "POST",
1465
825
  });
1466
826
 
1467
827
  if (!response.ok) {
1468
- throw new Error(`HTTP error! status: ${response.status}`);
828
+ const errorData = (await response.json().catch(() => ({}))) as {
829
+ error?: string;
830
+ };
831
+ throw new Error(
832
+ errorData.error || `HTTP error! status: ${response.status}`
833
+ );
1469
834
  }
1470
835
 
1471
- const data: CommandsResponse = await response.json();
836
+ const data: StartProcessResponse = await response.json();
1472
837
  console.log(
1473
- `[HTTP Client] Available commands: ${data.availableCommands.length}`
838
+ `[HTTP Client] Process started: ${command}, ID: ${data.process.id}`
1474
839
  );
1475
- return data.availableCommands;
840
+
841
+ return data;
1476
842
  } catch (error) {
1477
- console.error("[HTTP Client] Error getting commands:", error);
843
+ console.error("[HTTP Client] Error starting process:", error);
1478
844
  throw error;
1479
845
  }
1480
846
  }
1481
847
 
1482
- getSessionId(): string | null {
1483
- return this.sessionId;
1484
- }
848
+ async listProcesses(sessionId?: string): Promise<ListProcessesResponse> {
849
+ try {
850
+ const url = sessionId
851
+ ? `/api/process/list?session=${encodeURIComponent(sessionId)}`
852
+ : "/api/process/list";
853
+ const response = await this.doFetch(url, {
854
+ headers: {
855
+ "Content-Type": "application/json",
856
+ },
857
+ method: "GET",
858
+ });
1485
859
 
1486
- setSessionId(sessionId: string): void {
1487
- this.sessionId = sessionId;
1488
- }
860
+ if (!response.ok) {
861
+ const errorData = (await response.json().catch(() => ({}))) as {
862
+ error?: string;
863
+ };
864
+ throw new Error(
865
+ errorData.error || `HTTP error! status: ${response.status}`
866
+ );
867
+ }
1489
868
 
1490
- clearSession(): void {
1491
- this.sessionId = null;
869
+ const data: ListProcessesResponse = await response.json();
870
+ console.log(`[HTTP Client] Listed ${data.processes.length} processes`);
871
+
872
+ return data;
873
+ } catch (error) {
874
+ console.error("[HTTP Client] Error listing processes:", error);
875
+ throw error;
876
+ }
1492
877
  }
1493
- }
1494
878
 
1495
- // Example usage and utility functions
1496
- export function createClient(options?: HttpClientOptions): HttpClient {
1497
- return new HttpClient(options);
1498
- }
879
+ async getProcess(processId: string): Promise<GetProcessResponse> {
880
+ try {
881
+ const response = await this.doFetch(`/api/process/${processId}`, {
882
+ headers: {
883
+ "Content-Type": "application/json",
884
+ },
885
+ method: "GET",
886
+ });
1499
887
 
1500
- // Convenience function for quick command execution
1501
- export async function quickExecute(
1502
- command: string,
1503
- args: string[] = [],
1504
- options?: HttpClientOptions
1505
- ): Promise<ExecuteResponse> {
1506
- const client = createClient(options);
1507
- await client.createSession();
1508
-
1509
- try {
1510
- return await client.execute(command, args);
1511
- } finally {
1512
- client.clearSession();
1513
- }
1514
- }
888
+ if (!response.ok) {
889
+ const errorData = (await response.json().catch(() => ({}))) as {
890
+ error?: string;
891
+ };
892
+ throw new Error(
893
+ errorData.error || `HTTP error! status: ${response.status}`
894
+ );
895
+ }
1515
896
 
1516
- // Convenience function for quick streaming command execution
1517
- export async function quickExecuteStream(
1518
- command: string,
1519
- args: string[] = [],
1520
- options?: HttpClientOptions
1521
- ): Promise<void> {
1522
- const client = createClient(options);
1523
- await client.createSession();
1524
-
1525
- try {
1526
- await client.executeStream(command, args);
1527
- } finally {
1528
- client.clearSession();
1529
- }
1530
- }
897
+ const data: GetProcessResponse = await response.json();
898
+ console.log(
899
+ `[HTTP Client] Got process ${processId}: ${
900
+ data.process?.status || "not found"
901
+ }`
902
+ );
1531
903
 
1532
- // Convenience function for quick git checkout
1533
- export async function quickGitCheckout(
1534
- repoUrl: string,
1535
- branch: string = "main",
1536
- targetDir?: string,
1537
- options?: HttpClientOptions
1538
- ): Promise<GitCheckoutResponse> {
1539
- const client = createClient(options);
1540
- await client.createSession();
1541
-
1542
- try {
1543
- return await client.gitCheckout(repoUrl, branch, targetDir);
1544
- } finally {
1545
- client.clearSession();
904
+ return data;
905
+ } catch (error) {
906
+ console.error("[HTTP Client] Error getting process:", error);
907
+ throw error;
908
+ }
1546
909
  }
1547
- }
1548
910
 
1549
- // Convenience function for quick directory creation
1550
- export async function quickMkdir(
1551
- path: string,
1552
- recursive: boolean = false,
1553
- options?: HttpClientOptions
1554
- ): Promise<MkdirResponse> {
1555
- const client = createClient(options);
1556
- await client.createSession();
1557
-
1558
- try {
1559
- return await client.mkdir(path, recursive);
1560
- } finally {
1561
- client.clearSession();
1562
- }
1563
- }
911
+ async killProcess(
912
+ processId: string
913
+ ): Promise<{ success: boolean; message: string }> {
914
+ try {
915
+ const response = await this.doFetch(`/api/process/${processId}`, {
916
+ headers: {
917
+ "Content-Type": "application/json",
918
+ },
919
+ method: "DELETE",
920
+ });
1564
921
 
1565
- // Convenience function for quick streaming git checkout
1566
- export async function quickGitCheckoutStream(
1567
- repoUrl: string,
1568
- branch: string = "main",
1569
- targetDir?: string,
1570
- options?: HttpClientOptions
1571
- ): Promise<void> {
1572
- const client = createClient(options);
1573
- await client.createSession();
1574
-
1575
- try {
1576
- await client.gitCheckoutStream(repoUrl, branch, targetDir);
1577
- } finally {
1578
- client.clearSession();
1579
- }
1580
- }
922
+ if (!response.ok) {
923
+ const errorData = (await response.json().catch(() => ({}))) as {
924
+ error?: string;
925
+ };
926
+ throw new Error(
927
+ errorData.error || `HTTP error! status: ${response.status}`
928
+ );
929
+ }
1581
930
 
1582
- // Convenience function for quick streaming directory creation
1583
- export async function quickMkdirStream(
1584
- path: string,
1585
- recursive: boolean = false,
1586
- options?: HttpClientOptions
1587
- ): Promise<void> {
1588
- const client = createClient(options);
1589
- await client.createSession();
1590
-
1591
- try {
1592
- await client.mkdirStream(path, recursive);
1593
- } finally {
1594
- client.clearSession();
1595
- }
1596
- }
931
+ const data = (await response.json()) as {
932
+ success: boolean;
933
+ message: string;
934
+ };
935
+ console.log(`[HTTP Client] Killed process ${processId}`);
1597
936
 
1598
- // Convenience function for quick file writing
1599
- export async function quickWriteFile(
1600
- path: string,
1601
- content: string,
1602
- encoding: string = "utf-8",
1603
- options?: HttpClientOptions
1604
- ): Promise<WriteFileResponse> {
1605
- const client = createClient(options);
1606
- await client.createSession();
1607
-
1608
- try {
1609
- return await client.writeFile(path, content, encoding);
1610
- } finally {
1611
- client.clearSession();
937
+ return data;
938
+ } catch (error) {
939
+ console.error("[HTTP Client] Error killing process:", error);
940
+ throw error;
941
+ }
1612
942
  }
1613
- }
1614
943
 
1615
- // Convenience function for quick streaming file writing
1616
- export async function quickWriteFileStream(
1617
- path: string,
1618
- content: string,
1619
- encoding: string = "utf-8",
1620
- options?: HttpClientOptions
1621
- ): Promise<void> {
1622
- const client = createClient(options);
1623
- await client.createSession();
1624
-
1625
- try {
1626
- await client.writeFileStream(path, content, encoding);
1627
- } finally {
1628
- client.clearSession();
1629
- }
1630
- }
944
+ async killAllProcesses(sessionId?: string): Promise<{
945
+ success: boolean;
946
+ killedCount: number;
947
+ message: string;
948
+ }> {
949
+ try {
950
+ const url = sessionId
951
+ ? `/api/process/kill-all?session=${encodeURIComponent(sessionId)}`
952
+ : "/api/process/kill-all";
953
+ const response = await this.doFetch(url, {
954
+ headers: {
955
+ "Content-Type": "application/json",
956
+ },
957
+ method: "DELETE",
958
+ });
1631
959
 
1632
- // Convenience function for quick file deletion
1633
- export async function quickDeleteFile(
1634
- path: string,
1635
- options?: HttpClientOptions
1636
- ): Promise<DeleteFileResponse> {
1637
- const client = createClient(options);
1638
- await client.createSession();
1639
-
1640
- try {
1641
- return await client.deleteFile(path);
1642
- } finally {
1643
- client.clearSession();
1644
- }
1645
- }
960
+ if (!response.ok) {
961
+ const errorData = (await response.json().catch(() => ({}))) as {
962
+ error?: string;
963
+ };
964
+ throw new Error(
965
+ errorData.error || `HTTP error! status: ${response.status}`
966
+ );
967
+ }
1646
968
 
1647
- // Convenience function for quick streaming file deletion
1648
- export async function quickDeleteFileStream(
1649
- path: string,
1650
- options?: HttpClientOptions
1651
- ): Promise<void> {
1652
- const client = createClient(options);
1653
- await client.createSession();
1654
-
1655
- try {
1656
- await client.deleteFileStream(path);
1657
- } finally {
1658
- client.clearSession();
1659
- }
1660
- }
969
+ const data = (await response.json()) as {
970
+ success: boolean;
971
+ killedCount: number;
972
+ message: string;
973
+ };
974
+ console.log(`[HTTP Client] Killed ${data.killedCount} processes`);
1661
975
 
1662
- // Convenience function for quick file renaming
1663
- export async function quickRenameFile(
1664
- oldPath: string,
1665
- newPath: string,
1666
- options?: HttpClientOptions
1667
- ): Promise<RenameFileResponse> {
1668
- const client = createClient(options);
1669
- await client.createSession();
1670
-
1671
- try {
1672
- return await client.renameFile(oldPath, newPath);
1673
- } finally {
1674
- client.clearSession();
976
+ return data;
977
+ } catch (error) {
978
+ console.error("[HTTP Client] Error killing all processes:", error);
979
+ throw error;
980
+ }
1675
981
  }
1676
- }
1677
982
 
1678
- // Convenience function for quick streaming file renaming
1679
- export async function quickRenameFileStream(
1680
- oldPath: string,
1681
- newPath: string,
1682
- options?: HttpClientOptions
1683
- ): Promise<void> {
1684
- const client = createClient(options);
1685
- await client.createSession();
1686
-
1687
- try {
1688
- await client.renameFileStream(oldPath, newPath);
1689
- } finally {
1690
- client.clearSession();
1691
- }
1692
- }
983
+ async getProcessLogs(processId: string): Promise<GetProcessLogsResponse> {
984
+ try {
985
+ const response = await this.doFetch(`/api/process/${processId}/logs`, {
986
+ headers: {
987
+ "Content-Type": "application/json",
988
+ },
989
+ method: "GET",
990
+ });
991
+
992
+ if (!response.ok) {
993
+ const errorData = (await response.json().catch(() => ({}))) as {
994
+ error?: string;
995
+ };
996
+ throw new Error(
997
+ errorData.error || `HTTP error! status: ${response.status}`
998
+ );
999
+ }
1000
+
1001
+ const data: GetProcessLogsResponse = await response.json();
1002
+ console.log(`[HTTP Client] Got logs for process ${processId}`);
1693
1003
 
1694
- // Convenience function for quick file moving
1695
- export async function quickMoveFile(
1696
- sourcePath: string,
1697
- destinationPath: string,
1698
- options?: HttpClientOptions
1699
- ): Promise<MoveFileResponse> {
1700
- const client = createClient(options);
1701
- await client.createSession();
1702
-
1703
- try {
1704
- return await client.moveFile(sourcePath, destinationPath);
1705
- } finally {
1706
- client.clearSession();
1004
+ return data;
1005
+ } catch (error) {
1006
+ console.error("[HTTP Client] Error getting process logs:", error);
1007
+ throw error;
1008
+ }
1707
1009
  }
1708
- }
1709
1010
 
1710
- // Convenience function for quick streaming file moving
1711
- export async function quickMoveFileStream(
1712
- sourcePath: string,
1713
- destinationPath: string,
1714
- options?: HttpClientOptions
1715
- ): Promise<void> {
1716
- const client = createClient(options);
1717
- await client.createSession();
1718
-
1719
- try {
1720
- await client.moveFileStream(sourcePath, destinationPath);
1721
- } finally {
1722
- client.clearSession();
1011
+ async streamProcessLogs(
1012
+ processId: string,
1013
+ options?: { signal?: AbortSignal }
1014
+ ): Promise<ReadableStream<Uint8Array>> {
1015
+ try {
1016
+ const response = await this.doFetch(`/api/process/${processId}/stream`, {
1017
+ headers: {
1018
+ Accept: "text/event-stream",
1019
+ "Cache-Control": "no-cache",
1020
+ },
1021
+ method: "GET",
1022
+ signal: options?.signal,
1023
+ });
1024
+
1025
+ if (!response.ok) {
1026
+ const errorData = (await response.json().catch(() => ({}))) as {
1027
+ error?: string;
1028
+ };
1029
+ throw new Error(
1030
+ errorData.error || `HTTP error! status: ${response.status}`
1031
+ );
1032
+ }
1033
+
1034
+ if (!response.body) {
1035
+ throw new Error("No response body for streaming request");
1036
+ }
1037
+
1038
+ console.log(
1039
+ `[HTTP Client] Started streaming logs for process ${processId}`
1040
+ );
1041
+
1042
+ return response.body;
1043
+ } catch (error) {
1044
+ console.error("[HTTP Client] Error streaming process logs:", error);
1045
+ throw error;
1046
+ }
1723
1047
  }
1724
1048
  }