@cloudflare/sandbox 0.0.0-cbb7fcd → 0.0.0-cdb8197

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