@cloudflare/sandbox 0.0.0-8db6d29 → 0.0.0-9375c1e

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 (40) hide show
  1. package/CHANGELOG.md +179 -0
  2. package/Dockerfile +99 -11
  3. package/README.md +805 -24
  4. package/container_src/bun.lock +122 -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 +406 -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 +434 -2739
  14. package/container_src/isolation.ts +1038 -0
  15. package/container_src/jupyter-server.ts +579 -0
  16. package/container_src/jupyter-service.ts +461 -0
  17. package/container_src/jupyter_config.py +48 -0
  18. package/container_src/mime-processor.ts +255 -0
  19. package/container_src/package.json +9 -0
  20. package/container_src/shell-escape.ts +42 -0
  21. package/container_src/startup.sh +84 -0
  22. package/container_src/types.ts +131 -0
  23. package/package.json +5 -9
  24. package/src/client.ts +431 -1372
  25. package/src/errors.ts +218 -0
  26. package/src/index.ts +63 -128
  27. package/src/interpreter-types.ts +383 -0
  28. package/src/interpreter.ts +150 -0
  29. package/src/jupyter-client.ts +349 -0
  30. package/src/request-handler.ts +144 -0
  31. package/src/sandbox.ts +747 -0
  32. package/src/security.ts +113 -0
  33. package/src/sse-parser.ts +147 -0
  34. package/src/types.ts +502 -0
  35. package/tsconfig.json +1 -1
  36. package/tests/client.example.ts +0 -308
  37. package/tests/connection-test.ts +0 -81
  38. package/tests/simple-test.ts +0 -81
  39. package/tests/test1.ts +0 -281
  40. 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;
144
109
  timestamp: string;
145
110
  }
146
111
 
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;
112
+ interface GetExposedPortsResponse {
113
+ ports: ExposedPort[];
114
+ count: number;
115
+ timestamp: string;
116
+ }
117
+
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(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,22 +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> {
365
+ async mkdir(
366
+ path: string,
367
+ recursive: boolean = false,
368
+ sessionId: string
369
+ ): Promise<MkdirResponse> {
560
370
  try {
561
- const targetSessionId = sessionId || this.sessionId;
562
-
563
- const response = await this.doFetch(`/api/git/checkout/stream`, {
371
+ const response = await this.doFetch(`/api/mkdir`, {
564
372
  body: JSON.stringify({
565
- branch,
566
- repoUrl,
567
- sessionId: targetSessionId,
568
- targetDir,
569
- }),
373
+ path,
374
+ recursive,
375
+ sessionId,
376
+ } as MkdirRequest),
570
377
  headers: {
571
378
  "Content-Type": "application/json",
572
379
  },
@@ -582,119 +389,32 @@ export class HttpClient {
582
389
  );
583
390
  }
584
391
 
585
- if (!response.body) {
586
- throw new Error("No response body for streaming request");
587
- }
392
+ const data: MkdirResponse = await response.json();
393
+ console.log(
394
+ `[HTTP Client] Directory created: ${path}, Success: ${data.success}, Recursive: ${data.recursive}${sessionId ? ` in session: ${sessionId}` : ''}`
395
+ );
588
396
 
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
- }
397
+ return data;
673
398
  } 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
- );
399
+ console.error("[HTTP Client] Error creating directory:", error);
680
400
  throw error;
681
401
  }
682
402
  }
683
403
 
684
- async mkdir(
404
+ async writeFile(
685
405
  path: string,
686
- recursive: boolean = false,
687
- sessionId?: string
688
- ): Promise<MkdirResponse> {
406
+ content: string,
407
+ encoding: string = "utf-8",
408
+ sessionId: string
409
+ ): Promise<WriteFileResponse> {
689
410
  try {
690
- const targetSessionId = sessionId || this.sessionId;
691
-
692
- const response = await this.doFetch(`/api/mkdir`, {
411
+ const response = await this.doFetch(`/api/write`, {
693
412
  body: JSON.stringify({
413
+ content,
414
+ encoding,
694
415
  path,
695
- recursive,
696
- sessionId: targetSessionId,
697
- }),
416
+ sessionId,
417
+ } as WriteFileRequest),
698
418
  headers: {
699
419
  "Content-Type": "application/json",
700
420
  },
@@ -710,32 +430,30 @@ export class HttpClient {
710
430
  );
711
431
  }
712
432
 
713
- const data: MkdirResponse = await response.json();
433
+ const data: WriteFileResponse = await response.json();
714
434
  console.log(
715
- `[HTTP Client] Directory created: ${path}, Success: ${data.success}, Recursive: ${data.recursive}`
435
+ `[HTTP Client] File written: ${path}, Success: ${data.success}${sessionId ? ` in session: ${sessionId}` : ''}`
716
436
  );
717
437
 
718
438
  return data;
719
439
  } catch (error) {
720
- console.error("[HTTP Client] Error creating directory:", error);
440
+ console.error("[HTTP Client] Error writing file:", error);
721
441
  throw error;
722
442
  }
723
443
  }
724
444
 
725
- async mkdirStream(
445
+ async readFile(
726
446
  path: string,
727
- recursive: boolean = false,
728
- sessionId?: string
729
- ): Promise<void> {
447
+ encoding: string = "utf-8",
448
+ sessionId: string
449
+ ): Promise<ReadFileResponse> {
730
450
  try {
731
- const targetSessionId = sessionId || this.sessionId;
732
-
733
- const response = await this.doFetch(`/api/mkdir/stream`, {
451
+ const response = await this.doFetch(`/api/read`, {
734
452
  body: JSON.stringify({
453
+ encoding,
735
454
  path,
736
- recursive,
737
- sessionId: targetSessionId,
738
- }),
455
+ sessionId,
456
+ } as ReadFileRequest),
739
457
  headers: {
740
458
  "Content-Type": "application/json",
741
459
  },
@@ -751,117 +469,28 @@ export class HttpClient {
751
469
  );
752
470
  }
753
471
 
754
- if (!response.body) {
755
- throw new Error("No response body for streaming request");
756
- }
472
+ const data: ReadFileResponse = await response.json();
473
+ console.log(
474
+ `[HTTP Client] File read: ${path}, Success: ${data.success}, Content length: ${data.content.length}${sessionId ? ` in session: ${sessionId}` : ''}`
475
+ );
757
476
 
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
- }
477
+ return data;
838
478
  } 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
- );
479
+ console.error("[HTTP Client] Error reading file:", error);
845
480
  throw error;
846
481
  }
847
482
  }
848
483
 
849
- async writeFile(
484
+ async deleteFile(
850
485
  path: string,
851
- content: string,
852
- encoding: string = "utf-8",
853
- sessionId?: string
854
- ): Promise<WriteFileResponse> {
486
+ sessionId: string
487
+ ): Promise<DeleteFileResponse> {
855
488
  try {
856
- const targetSessionId = sessionId || this.sessionId;
857
-
858
- const response = await this.doFetch(`/api/write`, {
489
+ const response = await this.doFetch(`/api/delete`, {
859
490
  body: JSON.stringify({
860
- content,
861
- encoding,
862
491
  path,
863
- sessionId: targetSessionId,
864
- }),
492
+ sessionId,
493
+ } as DeleteFileRequest),
865
494
  headers: {
866
495
  "Content-Type": "application/json",
867
496
  },
@@ -877,34 +506,30 @@ export class HttpClient {
877
506
  );
878
507
  }
879
508
 
880
- const data: WriteFileResponse = await response.json();
509
+ const data: DeleteFileResponse = await response.json();
881
510
  console.log(
882
- `[HTTP Client] File written: ${path}, Success: ${data.success}`
511
+ `[HTTP Client] File deleted: ${path}, Success: ${data.success}${sessionId ? ` in session: ${sessionId}` : ''}`
883
512
  );
884
513
 
885
514
  return data;
886
515
  } catch (error) {
887
- console.error("[HTTP Client] Error writing file:", error);
516
+ console.error("[HTTP Client] Error deleting file:", error);
888
517
  throw error;
889
518
  }
890
519
  }
891
520
 
892
- async writeFileStream(
893
- path: string,
894
- content: string,
895
- encoding: string = "utf-8",
896
- sessionId?: string
897
- ): Promise<void> {
521
+ async renameFile(
522
+ oldPath: string,
523
+ newPath: string,
524
+ sessionId: string
525
+ ): Promise<RenameFileResponse> {
898
526
  try {
899
- const targetSessionId = sessionId || this.sessionId;
900
-
901
- const response = await this.doFetch(`/api/write/stream`, {
527
+ const response = await this.doFetch(`/api/rename`, {
902
528
  body: JSON.stringify({
903
- content,
904
- encoding,
905
- path,
906
- sessionId: targetSessionId,
907
- }),
529
+ newPath,
530
+ oldPath,
531
+ sessionId,
532
+ } as RenameFileRequest),
908
533
  headers: {
909
534
  "Content-Type": "application/json",
910
535
  },
@@ -920,114 +545,30 @@ export class HttpClient {
920
545
  );
921
546
  }
922
547
 
923
- if (!response.body) {
924
- throw new Error("No response body for streaming request");
925
- }
548
+ const data: RenameFileResponse = await response.json();
549
+ console.log(
550
+ `[HTTP Client] File renamed: ${oldPath} -> ${newPath}, Success: ${data.success}${sessionId ? ` in session: ${sessionId}` : ''}`
551
+ );
926
552
 
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
- }
553
+ return data;
1006
554
  } 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]
1012
- );
555
+ console.error("[HTTP Client] Error renaming file:", error);
1013
556
  throw error;
1014
557
  }
1015
558
  }
1016
559
 
1017
- async readFile(
1018
- path: string,
1019
- encoding: string = "utf-8",
1020
- sessionId?: string
1021
- ): Promise<ReadFileResponse> {
560
+ async moveFile(
561
+ sourcePath: string,
562
+ destinationPath: string,
563
+ sessionId: string
564
+ ): Promise<MoveFileResponse> {
1022
565
  try {
1023
- const targetSessionId = sessionId || this.sessionId;
1024
-
1025
- const response = await this.doFetch(`/api/read`, {
566
+ const response = await this.doFetch(`/api/move`, {
1026
567
  body: JSON.stringify({
1027
- encoding,
1028
- path,
1029
- sessionId: targetSessionId,
1030
- }),
568
+ destinationPath,
569
+ sourcePath,
570
+ sessionId,
571
+ } as MoveFileRequest),
1031
572
  headers: {
1032
573
  "Content-Type": "application/json",
1033
574
  },
@@ -1043,32 +584,33 @@ export class HttpClient {
1043
584
  );
1044
585
  }
1045
586
 
1046
- const data: ReadFileResponse = await response.json();
587
+ const data: MoveFileResponse = await response.json();
1047
588
  console.log(
1048
- `[HTTP Client] File read: ${path}, Success: ${data.success}, Content length: ${data.content.length}`
589
+ `[HTTP Client] File moved: ${sourcePath} -> ${destinationPath}, Success: ${data.success}${sessionId ? ` in session: ${sessionId}` : ''}`
1049
590
  );
1050
591
 
1051
592
  return data;
1052
593
  } catch (error) {
1053
- console.error("[HTTP Client] Error reading file:", error);
594
+ console.error("[HTTP Client] Error moving file:", error);
1054
595
  throw error;
1055
596
  }
1056
597
  }
1057
598
 
1058
- async readFileStream(
599
+ async listFiles(
1059
600
  path: string,
1060
- encoding: string = "utf-8",
1061
- sessionId?: string
1062
- ): Promise<void> {
601
+ sessionId: string,
602
+ options?: {
603
+ recursive?: boolean;
604
+ includeHidden?: boolean;
605
+ }
606
+ ): Promise<ListFilesResponse> {
1063
607
  try {
1064
- const targetSessionId = sessionId || this.sessionId;
1065
-
1066
- const response = await this.doFetch(`/api/read/stream`, {
608
+ const response = await this.doFetch(`/api/list-files`, {
1067
609
  body: JSON.stringify({
1068
- encoding,
1069
610
  path,
1070
- sessionId: targetSessionId,
1071
- }),
611
+ options,
612
+ sessionId,
613
+ } as ListFilesRequest),
1072
614
  headers: {
1073
615
  "Content-Type": "application/json",
1074
616
  },
@@ -1084,105 +626,24 @@ export class HttpClient {
1084
626
  );
1085
627
  }
1086
628
 
1087
- if (!response.body) {
1088
- throw new Error("No response body for streaming request");
1089
- }
629
+ const data: ListFilesResponse = await response.json();
630
+ console.log(
631
+ `[HTTP Client] Listed ${data.files.length} files in: ${path}, Success: ${data.success}${sessionId ? ` in session: ${sessionId}` : ''}`
632
+ );
1090
633
 
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
- }
634
+ return data;
1164
635
  } 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
- );
636
+ console.error("[HTTP Client] Error listing files:", error);
1171
637
  throw error;
1172
638
  }
1173
639
  }
1174
640
 
1175
- async deleteFile(
1176
- path: string,
1177
- sessionId?: string
1178
- ): Promise<DeleteFileResponse> {
641
+ async exposePort(port: number, name?: string): Promise<ExposePortResponse> {
1179
642
  try {
1180
- const targetSessionId = sessionId || this.sessionId;
1181
-
1182
- const response = await this.doFetch(`/api/delete`, {
643
+ const response = await this.doFetch(`/api/expose-port`, {
1183
644
  body: JSON.stringify({
1184
- path,
1185
- sessionId: targetSessionId,
645
+ port,
646
+ name,
1186
647
  }),
1187
648
  headers: {
1188
649
  "Content-Type": "application/json",
@@ -1194,36 +655,36 @@ export class HttpClient {
1194
655
  const errorData = (await response.json().catch(() => ({}))) as {
1195
656
  error?: string;
1196
657
  };
658
+ console.log(errorData);
1197
659
  throw new Error(
1198
660
  errorData.error || `HTTP error! status: ${response.status}`
1199
661
  );
1200
662
  }
1201
663
 
1202
- const data: DeleteFileResponse = await response.json();
664
+ const data: ExposePortResponse = await response.json();
1203
665
  console.log(
1204
- `[HTTP Client] File deleted: ${path}, Success: ${data.success}`
666
+ `[HTTP Client] Port exposed: ${port}${
667
+ name ? ` (${name})` : ""
668
+ }, Success: ${data.success}`
1205
669
  );
1206
670
 
1207
671
  return data;
1208
672
  } catch (error) {
1209
- console.error("[HTTP Client] Error deleting file:", error);
673
+ console.error("[HTTP Client] Error exposing port:", error);
1210
674
  throw error;
1211
675
  }
1212
676
  }
1213
677
 
1214
- async deleteFileStream(path: string, sessionId?: string): Promise<void> {
678
+ async unexposePort(port: number): Promise<UnexposePortResponse> {
1215
679
  try {
1216
- const targetSessionId = sessionId || this.sessionId;
1217
-
1218
- const response = await this.doFetch(`/api/delete/stream`, {
680
+ const response = await this.doFetch(`/api/unexpose-port`, {
1219
681
  body: JSON.stringify({
1220
- path,
1221
- sessionId: targetSessionId,
682
+ port,
1222
683
  }),
1223
684
  headers: {
1224
685
  "Content-Type": "application/json",
1225
686
  },
1226
- method: "POST",
687
+ method: "DELETE",
1227
688
  });
1228
689
 
1229
690
  if (!response.ok) {
@@ -1235,101 +696,89 @@ export class HttpClient {
1235
696
  );
1236
697
  }
1237
698
 
1238
- if (!response.body) {
1239
- throw new Error("No response body for streaming request");
1240
- }
699
+ const data: UnexposePortResponse = await response.json();
700
+ console.log(
701
+ `[HTTP Client] Port unexposed: ${port}, Success: ${data.success}`
702
+ );
703
+
704
+ return data;
705
+ } catch (error) {
706
+ console.error("[HTTP Client] Error unexposing port:", error);
707
+ throw error;
708
+ }
709
+ }
710
+
711
+ async getExposedPorts(): Promise<GetExposedPortsResponse> {
712
+ try {
713
+ const response = await this.doFetch(`/api/exposed-ports`, {
714
+ headers: {
715
+ "Content-Type": "application/json",
716
+ },
717
+ method: "GET",
718
+ });
1241
719
 
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();
720
+ if (!response.ok) {
721
+ const errorData = (await response.json().catch(() => ({}))) as {
722
+ error?: string;
723
+ };
724
+ throw new Error(
725
+ errorData.error || `HTTP error! status: ${response.status}`
726
+ );
1307
727
  }
728
+
729
+ const data: GetExposedPortsResponse = await response.json();
730
+ console.log(`[HTTP Client] Got ${data.count} exposed ports`);
731
+
732
+ return data;
1308
733
  } 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
- );
734
+ console.error("[HTTP Client] Error getting exposed ports:", error);
1315
735
  throw error;
1316
736
  }
1317
737
  }
1318
738
 
1319
- async renameFile(
1320
- oldPath: string,
1321
- newPath: string,
1322
- sessionId?: string
1323
- ): Promise<RenameFileResponse> {
739
+ async ping(): Promise<string> {
1324
740
  try {
1325
- const targetSessionId = sessionId || this.sessionId;
741
+ const response = await this.doFetch(`/api/ping`, {
742
+ headers: {
743
+ "Content-Type": "application/json",
744
+ },
745
+ method: "GET",
746
+ });
1326
747
 
1327
- const response = await this.doFetch(`/api/rename`, {
748
+ if (!response.ok) {
749
+ throw new Error(`HTTP error! status: ${response.status}`);
750
+ }
751
+
752
+ const data: PingResponse = await response.json();
753
+ console.log(`[HTTP Client] Ping response: ${data.message}`);
754
+ return data.timestamp;
755
+ } catch (error) {
756
+ console.error("[HTTP Client] Error pinging server:", error);
757
+ throw error;
758
+ }
759
+ }
760
+
761
+
762
+ // Process management methods
763
+ async startProcess(
764
+ command: string,
765
+ sessionId: string,
766
+ options?: {
767
+ processId?: string;
768
+ timeout?: number;
769
+ env?: Record<string, string>;
770
+ cwd?: string;
771
+ encoding?: string;
772
+ autoCleanup?: boolean;
773
+ }
774
+ ): Promise<StartProcessResponse> {
775
+ try {
776
+ const response = await this.doFetch("/api/process/start", {
1328
777
  body: JSON.stringify({
1329
- newPath,
1330
- oldPath,
1331
- sessionId: targetSessionId,
1332
- }),
778
+ command,
779
+ sessionId,
780
+ options,
781
+ } as StartProcessRequest),
1333
782
  headers: {
1334
783
  "Content-Type": "application/json",
1335
784
  },
@@ -1345,36 +794,28 @@ export class HttpClient {
1345
794
  );
1346
795
  }
1347
796
 
1348
- const data: RenameFileResponse = await response.json();
797
+ const data: StartProcessResponse = await response.json();
1349
798
  console.log(
1350
- `[HTTP Client] File renamed: ${oldPath} -> ${newPath}, Success: ${data.success}`
799
+ `[HTTP Client] Process started: ${command}, ID: ${data.process.id}`
1351
800
  );
1352
801
 
1353
802
  return data;
1354
803
  } catch (error) {
1355
- console.error("[HTTP Client] Error renaming file:", error);
804
+ console.error("[HTTP Client] Error starting process:", error);
1356
805
  throw error;
1357
806
  }
1358
807
  }
1359
808
 
1360
- async renameFileStream(
1361
- oldPath: string,
1362
- newPath: string,
1363
- sessionId?: string
1364
- ): Promise<void> {
809
+ async listProcesses(sessionId?: string): Promise<ListProcessesResponse> {
1365
810
  try {
1366
- const targetSessionId = sessionId || this.sessionId;
1367
-
1368
- const response = await this.doFetch(`/api/rename/stream`, {
1369
- body: JSON.stringify({
1370
- newPath,
1371
- oldPath,
1372
- sessionId: targetSessionId,
1373
- }),
811
+ const url = sessionId
812
+ ? `/api/process/list?session=${encodeURIComponent(sessionId)}`
813
+ : "/api/process/list";
814
+ const response = await this.doFetch(url, {
1374
815
  headers: {
1375
816
  "Content-Type": "application/json",
1376
817
  },
1377
- method: "POST",
818
+ method: "GET",
1378
819
  });
1379
820
 
1380
821
  if (!response.ok) {
@@ -1386,108 +827,23 @@ export class HttpClient {
1386
827
  );
1387
828
  }
1388
829
 
1389
- if (!response.body) {
1390
- throw new Error("No response body for streaming request");
1391
- }
830
+ const data: ListProcessesResponse = await response.json();
831
+ console.log(`[HTTP Client] Listed ${data.processes.length} processes`);
1392
832
 
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
- }
833
+ return data;
1462
834
  } 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
- );
835
+ console.error("[HTTP Client] Error listing processes:", error);
1469
836
  throw error;
1470
837
  }
1471
838
  }
1472
839
 
1473
- async moveFile(
1474
- sourcePath: string,
1475
- destinationPath: string,
1476
- sessionId?: string
1477
- ): Promise<MoveFileResponse> {
840
+ async getProcess(processId: string): Promise<GetProcessResponse> {
1478
841
  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
- }),
842
+ const response = await this.doFetch(`/api/process/${processId}`, {
1487
843
  headers: {
1488
844
  "Content-Type": "application/json",
1489
845
  },
1490
- method: "POST",
846
+ method: "GET",
1491
847
  });
1492
848
 
1493
849
  if (!response.ok) {
@@ -1499,36 +855,29 @@ export class HttpClient {
1499
855
  );
1500
856
  }
1501
857
 
1502
- const data: MoveFileResponse = await response.json();
858
+ const data: GetProcessResponse = await response.json();
1503
859
  console.log(
1504
- `[HTTP Client] File moved: ${sourcePath} -> ${destinationPath}, Success: ${data.success}`
860
+ `[HTTP Client] Got process ${processId}: ${
861
+ data.process?.status || "not found"
862
+ }`
1505
863
  );
1506
864
 
1507
865
  return data;
1508
866
  } catch (error) {
1509
- console.error("[HTTP Client] Error moving file:", error);
867
+ console.error("[HTTP Client] Error getting process:", error);
1510
868
  throw error;
1511
869
  }
1512
870
  }
1513
871
 
1514
- async moveFileStream(
1515
- sourcePath: string,
1516
- destinationPath: string,
1517
- sessionId?: string
1518
- ): Promise<void> {
872
+ async killProcess(
873
+ processId: string
874
+ ): Promise<{ success: boolean; message: string }> {
1519
875
  try {
1520
- const targetSessionId = sessionId || this.sessionId;
1521
-
1522
- const response = await this.doFetch(`/api/move/stream`, {
1523
- body: JSON.stringify({
1524
- destinationPath,
1525
- sessionId: targetSessionId,
1526
- sourcePath,
1527
- }),
876
+ const response = await this.doFetch(`/api/process/${processId}`, {
1528
877
  headers: {
1529
878
  "Content-Type": "application/json",
1530
879
  },
1531
- method: "POST",
880
+ method: "DELETE",
1532
881
  });
1533
882
 
1534
883
  if (!response.ok) {
@@ -1540,118 +889,61 @@ export class HttpClient {
1540
889
  );
1541
890
  }
1542
891
 
1543
- if (!response.body) {
1544
- throw new Error("No response body for streaming request");
1545
- }
892
+ const data = (await response.json()) as {
893
+ success: boolean;
894
+ message: string;
895
+ };
896
+ console.log(`[HTTP Client] Killed process ${processId}`);
1546
897
 
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
- }
898
+ return data;
1619
899
  } 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
- );
900
+ console.error("[HTTP Client] Error killing process:", error);
1626
901
  throw error;
1627
902
  }
1628
903
  }
1629
904
 
1630
- async ping(): Promise<string> {
905
+ async killAllProcesses(sessionId?: string): Promise<{
906
+ success: boolean;
907
+ killedCount: number;
908
+ message: string;
909
+ }> {
1631
910
  try {
1632
- const response = await this.doFetch(`/api/ping`, {
911
+ const url = sessionId
912
+ ? `/api/process/kill-all?session=${encodeURIComponent(sessionId)}`
913
+ : "/api/process/kill-all";
914
+ const response = await this.doFetch(url, {
1633
915
  headers: {
1634
916
  "Content-Type": "application/json",
1635
917
  },
1636
- method: "GET",
918
+ method: "DELETE",
1637
919
  });
1638
920
 
1639
921
  if (!response.ok) {
1640
- throw new Error(`HTTP error! status: ${response.status}`);
922
+ const errorData = (await response.json().catch(() => ({}))) as {
923
+ error?: string;
924
+ };
925
+ throw new Error(
926
+ errorData.error || `HTTP error! status: ${response.status}`
927
+ );
1641
928
  }
1642
929
 
1643
- const data: PingResponse = await response.json();
1644
- console.log(`[HTTP Client] Ping response: ${data.message}`);
1645
- return data.timestamp;
930
+ const data = (await response.json()) as {
931
+ success: boolean;
932
+ killedCount: number;
933
+ message: string;
934
+ };
935
+ console.log(`[HTTP Client] Killed ${data.killedCount} processes`);
936
+
937
+ return data;
1646
938
  } catch (error) {
1647
- console.error("[HTTP Client] Error pinging server:", error);
939
+ console.error("[HTTP Client] Error killing all processes:", error);
1648
940
  throw error;
1649
941
  }
1650
942
  }
1651
943
 
1652
- async getCommands(): Promise<string[]> {
944
+ async getProcessLogs(processId: string): Promise<GetProcessLogsResponse> {
1653
945
  try {
1654
- const response = await fetch(`${this.baseUrl}/api/commands`, {
946
+ const response = await this.doFetch(`/api/process/${processId}/logs`, {
1655
947
  headers: {
1656
948
  "Content-Type": "application/json",
1657
949
  },
@@ -1659,292 +951,59 @@ export class HttpClient {
1659
951
  });
1660
952
 
1661
953
  if (!response.ok) {
1662
- throw new Error(`HTTP error! status: ${response.status}`);
954
+ const errorData = (await response.json().catch(() => ({}))) as {
955
+ error?: string;
956
+ };
957
+ throw new Error(
958
+ errorData.error || `HTTP error! status: ${response.status}`
959
+ );
1663
960
  }
1664
961
 
1665
- const data: CommandsResponse = await response.json();
1666
- console.log(
1667
- `[HTTP Client] Available commands: ${data.availableCommands.length}`
1668
- );
1669
- return data.availableCommands;
962
+ const data: GetProcessLogsResponse = await response.json();
963
+ console.log(`[HTTP Client] Got logs for process ${processId}`);
964
+
965
+ return data;
1670
966
  } catch (error) {
1671
- console.error("[HTTP Client] Error getting commands:", error);
967
+ console.error("[HTTP Client] Error getting process logs:", error);
1672
968
  throw error;
1673
969
  }
1674
970
  }
1675
971
 
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
- }
1693
-
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
- }
1709
-
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
- }
1725
-
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();
1740
- }
1741
- }
1742
-
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
- }
1758
-
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
- }
1775
-
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
- }
1791
-
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();
1806
- }
1807
- }
1808
-
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
- }
1825
-
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
- }
1841
-
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
- }
1857
-
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();
1870
- }
1871
- }
1872
-
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
- }
972
+ async streamProcessLogs(
973
+ processId: string,
974
+ options?: { signal?: AbortSignal }
975
+ ): Promise<ReadableStream<Uint8Array>> {
976
+ try {
977
+ const response = await this.doFetch(`/api/process/${processId}/stream`, {
978
+ headers: {
979
+ Accept: "text/event-stream",
980
+ "Cache-Control": "no-cache",
981
+ },
982
+ method: "GET",
983
+ signal: options?.signal,
984
+ });
1887
985
 
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
- }
986
+ if (!response.ok) {
987
+ const errorData = (await response.json().catch(() => ({}))) as {
988
+ error?: string;
989
+ };
990
+ throw new Error(
991
+ errorData.error || `HTTP error! status: ${response.status}`
992
+ );
993
+ }
1903
994
 
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
- }
995
+ if (!response.body) {
996
+ throw new Error("No response body for streaming request");
997
+ }
1919
998
 
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
- }
999
+ console.log(
1000
+ `[HTTP Client] Started streaming logs for process ${processId}`
1001
+ );
1935
1002
 
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();
1003
+ return response.body;
1004
+ } catch (error) {
1005
+ console.error("[HTTP Client] Error streaming process logs:", error);
1006
+ throw error;
1007
+ }
1949
1008
  }
1950
1009
  }