@cloudflare/sandbox 0.0.0-ee8c772 → 0.0.0-ef9e320

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