rl-rockcli 0.0.6 → 0.0.8

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 (74) hide show
  1. package/index.js +51 -20
  2. package/package.json +2 -2
  3. package/commands/log/core/constants.js +0 -237
  4. package/commands/log/core/display.js +0 -370
  5. package/commands/log/core/search.js +0 -330
  6. package/commands/log/core/tail.js +0 -216
  7. package/commands/log/core/utils.js +0 -424
  8. package/commands/log.js +0 -298
  9. package/commands/sandbox/core/log-bridge.js +0 -119
  10. package/commands/sandbox/core/replay/analyzer.js +0 -311
  11. package/commands/sandbox/core/replay/batch-orchestrator.js +0 -536
  12. package/commands/sandbox/core/replay/batch-task.js +0 -369
  13. package/commands/sandbox/core/replay/concurrent-display.js +0 -70
  14. package/commands/sandbox/core/replay/concurrent-orchestrator.js +0 -170
  15. package/commands/sandbox/core/replay/data-source.js +0 -86
  16. package/commands/sandbox/core/replay/display.js +0 -231
  17. package/commands/sandbox/core/replay/executor.js +0 -634
  18. package/commands/sandbox/core/replay/history-fetcher.js +0 -124
  19. package/commands/sandbox/core/replay/index.js +0 -338
  20. package/commands/sandbox/core/replay/loghouse-data-source.js +0 -177
  21. package/commands/sandbox/core/replay/pid-mapping.js +0 -26
  22. package/commands/sandbox/core/replay/request.js +0 -109
  23. package/commands/sandbox/core/replay/worker.js +0 -166
  24. package/commands/sandbox/core/session.js +0 -346
  25. package/commands/sandbox/log-bridge.js +0 -2
  26. package/commands/sandbox/ray.js +0 -2
  27. package/commands/sandbox/replay/analyzer.js +0 -311
  28. package/commands/sandbox/replay/batch-orchestrator.js +0 -536
  29. package/commands/sandbox/replay/batch-task.js +0 -369
  30. package/commands/sandbox/replay/concurrent-display.js +0 -70
  31. package/commands/sandbox/replay/concurrent-orchestrator.js +0 -170
  32. package/commands/sandbox/replay/display.js +0 -231
  33. package/commands/sandbox/replay/executor.js +0 -634
  34. package/commands/sandbox/replay/history-fetcher.js +0 -118
  35. package/commands/sandbox/replay/index.js +0 -338
  36. package/commands/sandbox/replay/pid-mapping.js +0 -26
  37. package/commands/sandbox/replay/request.js +0 -109
  38. package/commands/sandbox/replay/worker.js +0 -166
  39. package/commands/sandbox/replay.js +0 -2
  40. package/commands/sandbox/session.js +0 -2
  41. package/commands/sandbox-original.js +0 -1393
  42. package/commands/sandbox.js +0 -499
  43. package/help/help.json +0 -1071
  44. package/help/middleware.js +0 -71
  45. package/help/renderer.js +0 -800
  46. package/lib/plugin-context.js +0 -40
  47. package/sdks/sandbox/core/client.js +0 -845
  48. package/sdks/sandbox/core/config.js +0 -70
  49. package/sdks/sandbox/core/types.js +0 -74
  50. package/sdks/sandbox/httpLogger.js +0 -251
  51. package/sdks/sandbox/index.js +0 -9
  52. package/utils/asciiArt.js +0 -138
  53. package/utils/bun-compat.js +0 -59
  54. package/utils/ciPipelines.js +0 -138
  55. package/utils/cli.js +0 -17
  56. package/utils/command-router.js +0 -79
  57. package/utils/configManager.js +0 -503
  58. package/utils/dependency-resolver.js +0 -135
  59. package/utils/eagleeye_traceid.js +0 -151
  60. package/utils/envDetector.js +0 -78
  61. package/utils/execution_logger.js +0 -415
  62. package/utils/featureManager.js +0 -68
  63. package/utils/firstTimeTip.js +0 -44
  64. package/utils/hook-manager.js +0 -125
  65. package/utils/http-logger.js +0 -264
  66. package/utils/i18n.js +0 -139
  67. package/utils/image-progress.js +0 -159
  68. package/utils/logger.js +0 -154
  69. package/utils/plugin-loader.js +0 -124
  70. package/utils/plugin-manager.js +0 -348
  71. package/utils/ray_cli_wrapper.js +0 -746
  72. package/utils/sandbox-client.js +0 -419
  73. package/utils/terminal.js +0 -32
  74. package/utils/tips.js +0 -106
@@ -1,845 +0,0 @@
1
- const axios = require('axios');
2
- const { SandboxConfig } = require('./config');
3
- const logger = require('../../../utils/logger');
4
- const { httpLogger } = require('../httpLogger');
5
-
6
- /**
7
- * Sandbox Client for managing sandbox environments
8
- */
9
- class SandboxClient {
10
- constructor(config, options = {}) {
11
- if (config instanceof SandboxConfig) {
12
- this.config = config;
13
- } else {
14
- this.config = new SandboxConfig(config);
15
- }
16
- this.config.validate({ requireImage: options.requireImage !== false });
17
-
18
- this._sandboxId = null;
19
- this._hostName = null;
20
- this._hostIp = null;
21
- this._url = `${this.config.baseUrl}/apis/envs/sandbox/v1`;
22
- this._onLog = options.onLog || null;
23
-
24
- // Allow injection of custom headers builder for internal/external environments
25
- // Core version only builds basic headers; internal version can inject additional headers
26
- this._buildHeadersFn = options.buildHeaders || null;
27
-
28
- // Create axios instance with interceptors for HTTP logging
29
- this._axios = axios.create();
30
- this._setupInterceptors();
31
- }
32
-
33
- /**
34
- * Log a message, either through callback or directly
35
- * @private
36
- * @param {string} level - Log level ('info', 'warn', 'debug')
37
- * @param {string} message - Log message
38
- */
39
- _log(level, message) {
40
- if (this._onLog) {
41
- this._onLog(level, message);
42
- } else {
43
- logger[level](message);
44
- }
45
- }
46
-
47
- /**
48
- * Setup axios interceptors for HTTP logging
49
- */
50
- _setupInterceptors() {
51
- // Request interceptor
52
- this._axios.interceptors.request.use((config) => {
53
- config._startTime = Date.now();
54
- httpLogger.logRequest(
55
- config.method?.toUpperCase() || 'GET',
56
- config.url,
57
- config.data
58
- );
59
- return config;
60
- });
61
-
62
- // Response interceptor
63
- this._axios.interceptors.response.use(
64
- (response) => {
65
- const duration = Date.now() - (response.config._startTime || Date.now());
66
- // Extract trace ID from response headers
67
- const traceId = this._extractTraceId(response.headers);
68
- httpLogger.logResponse(
69
- response.config.method?.toUpperCase() || 'GET',
70
- response.config.url,
71
- response.status,
72
- duration,
73
- response.data,
74
- traceId
75
- );
76
- return response;
77
- },
78
- (error) => {
79
- const duration = Date.now() - (error.config?._startTime || Date.now());
80
- // Try to extract trace ID from error response
81
- const traceId = error.response ? this._extractTraceId(error.response.headers) : null;
82
- httpLogger.logError(
83
- error.config?.method?.toUpperCase() || 'GET',
84
- error.config?.url || 'unknown',
85
- error,
86
- duration,
87
- traceId
88
- );
89
- return Promise.reject(error);
90
- }
91
- );
92
- }
93
-
94
- /**
95
- * Extract trace ID from response headers
96
- * Checks common trace ID header names
97
- */
98
- _extractTraceId(headers) {
99
- if (!headers) return null;
100
-
101
- // Common trace ID header names (case-insensitive in axios)
102
- const traceIdHeaders = [
103
- 'x-trace-id',
104
- 'x-request-id',
105
- 'traceid',
106
- 'trace-id',
107
- 'request-id',
108
- 'x-b3-traceid',
109
- ];
110
-
111
- for (const headerName of traceIdHeaders) {
112
- const value = headers[headerName];
113
- if (value) return value;
114
- }
115
-
116
- return null;
117
- }
118
-
119
- get sandboxId() {
120
- return this._sandboxId;
121
- }
122
-
123
- get hostName() {
124
- return this._hostName;
125
- }
126
-
127
- get hostIp() {
128
- return this._hostIp;
129
- }
130
-
131
- _buildHeaders() {
132
- // Core headers - common to all environments
133
- const headers = {
134
- 'Content-Type': 'application/json',
135
- };
136
-
137
- // XRL-Authorization is a common authentication mechanism
138
- if (this.config.xrlAuthorization) {
139
- headers['XRL-Authorization'] = `Bearer ${this.config.xrlAuthorization}`;
140
- }
141
-
142
- // Allow extra headers to be passed in config
143
- if (this.config.extraHeaders) {
144
- Object.assign(headers, this.config.extraHeaders);
145
- }
146
-
147
- // Inject additional headers from custom builder (e.g., internal headers)
148
- if (this._buildHeadersFn) {
149
- const additionalHeaders = this._buildHeadersFn(this.config);
150
- Object.assign(headers, additionalHeaders);
151
- }
152
-
153
- return headers;
154
- }
155
-
156
- /**
157
- * Start a new sandbox instance
158
- * @returns {Promise<Object>} Sandbox instance info
159
- */
160
- async start() {
161
- const url = `${this._url}/start_async`;
162
- const headers = this._buildHeaders();
163
- const data = {
164
- image: this.config.image,
165
- auto_clear_time: Math.floor(this.config.autoClearSeconds / 60),
166
- auto_clear_time_minutes: Math.floor(this.config.autoClearSeconds / 60),
167
- startup_timeout: this.config.startupTimeout,
168
- memory: this.config.memory,
169
- cpus: this.config.cpus,
170
- };
171
-
172
- logger.debug(`[Sandbox SDK] POST ${url}`);
173
- logger.debug(`[Sandbox SDK] Request headers: ${JSON.stringify(headers)}`);
174
- logger.debug(`[Sandbox SDK] Request body: ${JSON.stringify(data)}`);
175
-
176
- const response = await this._axios.post(url, data, { headers });
177
- logger.debug(`[Sandbox SDK] Response: ${JSON.stringify(response.data)}`);
178
-
179
- if (response.data.status !== 'Success') {
180
- const error = response.data.result || response.data;
181
- throw new Error(`Failed to start sandbox: ${JSON.stringify(error)}`);
182
- }
183
-
184
- const result = response.data.result;
185
- this._sandboxId = result.sandbox_id;
186
- this._hostName = result.host_name;
187
- this._hostIp = result.host_ip;
188
-
189
- let isAlive = false;
190
- if (this.config.waitForAlive) {
191
- // Wait for sandbox to be alive
192
- await this._waitForAlive();
193
- isAlive = true;
194
- }
195
-
196
- return {
197
- sandboxId: this._sandboxId,
198
- hostName: this._hostName,
199
- hostIp: this._hostIp,
200
- isAlive: isAlive,
201
- };
202
- }
203
-
204
- async _waitForAlive() {
205
- const startTime = Date.now();
206
- const timeout = this.config.startupTimeout * 1000;
207
- let lastStatus = null;
208
- let lastLogTime = 0;
209
- const loadingChars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
210
- let loadingIndex = 0;
211
-
212
- this._log('info', 'Waiting for sandbox to be ready...');
213
-
214
- while (Date.now() - startTime < timeout) {
215
- const status = await this.getStatus();
216
-
217
- if (status.is_alive) {
218
- this._log('info', `Sandbox is ready (ID: ${this._sandboxId})`);
219
- return;
220
- }
221
-
222
- // Show progress for status changes
223
- if (status.status && JSON.stringify(status.status) !== JSON.stringify(lastStatus)) {
224
- lastStatus = status.status;
225
- const stageInfo = Object.entries(status.status)
226
- .map(([stage, details]) => {
227
- const icon = details.status === 'success' ? '✓' :
228
- details.status === 'failed' ? '✗' :
229
- details.status === 'running' ? '⟳' : '○';
230
- return `${stage}: ${icon}`;
231
- })
232
- .join(', ');
233
- this._log('info', `Sandbox startup progress: ${stageInfo}`);
234
- }
235
-
236
- // Show loading animation every 3 seconds if no status change
237
- const now = Date.now();
238
- if (now - lastLogTime > 3000) {
239
- loadingIndex = (loadingIndex + 1) % loadingChars.length;
240
- const elapsed = Math.floor((now - startTime) / 1000);
241
- this._log('info', `${loadingChars[loadingIndex]} Starting sandbox... (${elapsed}s elapsed)`);
242
- lastLogTime = now;
243
- }
244
-
245
- // Check for failures
246
- if (status.status) {
247
- for (const [stage, details] of Object.entries(status.status)) {
248
- if (details.status === 'failed' || details.status === 'timeout') {
249
- // Ignore ray_schedule failure during wait for alive
250
- if (stage === 'ray_schedule') {
251
- this._log('debug', `Ignoring ray_schedule failure during wait for alive: ${details.message || 'Unknown error'}`);
252
- continue;
253
- }
254
- throw new Error(`Sandbox failed at ${stage}: ${details.message || 'Unknown error'}`);
255
- }
256
- }
257
- }
258
-
259
- await new Promise(resolve => setTimeout(resolve, 3000));
260
- }
261
-
262
- throw new Error(`Sandbox did not become alive within ${this.config.startupTimeout}s`);
263
- }
264
-
265
- /**
266
- * Get sandbox status
267
- * @returns {Promise<Object>} Sandbox status
268
- */
269
- async getStatus() {
270
- const url = `${this._url}/get_status?sandbox_id=${this._sandboxId}`;
271
- const headers = this._buildHeaders();
272
-
273
- logger.debug(`[Sandbox SDK] GET ${url}`);
274
- logger.debug(`[Sandbox SDK] Request headers: ${JSON.stringify(headers)}`);
275
-
276
- const response = await this._axios.get(url, { headers });
277
- logger.debug(`[Sandbox SDK] Response headers: ${JSON.stringify(response.headers)}`);
278
- logger.debug(`[Sandbox SDK] Response: ${JSON.stringify(response.data)}`);
279
-
280
- if (response.data.status !== 'Success') {
281
- throw new Error(`Failed to get status: ${JSON.stringify(response.data)}`);
282
- }
283
-
284
- const result = response.data.result;
285
- // 从响应头中提取 cluster 和其他有用信息
286
- result.cluster = response.headers['x-rock-gateway-target-cluster'] || 'N/A';
287
- result.request_id = response.headers['x-request-id'] || response.headers['request-id'] || 'N/A';
288
- result.eagleeye_traceid = response.headers['eagleeye-traceid'] || 'N/A';
289
-
290
- return result;
291
- }
292
-
293
- /**
294
- * Check if sandbox is alive
295
- * @returns {Promise<Object>} IsAlive response
296
- */
297
- async isAlive() {
298
- try {
299
- const status = await this.getStatus();
300
- return {
301
- isAlive: status.is_alive,
302
- message: status.host_name || '',
303
- };
304
- } catch (error) {
305
- logger.warn(`Failed to check isAlive: ${error.message}`);
306
- throw new Error(`Failed to check isAlive: ${error.message}`);
307
- }
308
- }
309
-
310
- /**
311
- * Execute a command in the sandbox
312
- * @param {string|Array} command - Command to execute
313
- * @returns {Promise<Object>} Command response
314
- */
315
- async execute(command) {
316
- const url = `${this._url}/execute`;
317
- const headers = this._buildHeaders();
318
-
319
- let commandArray;
320
- if (Array.isArray(command)) {
321
- // Array command: use as-is
322
- commandArray = command;
323
- } else {
324
- // String command: wrap with sh -c to support shell operators (&&, ||, ;, |, etc.)
325
- commandArray = ['sh', '-c', command];
326
- }
327
-
328
- const data = {
329
- command: commandArray,
330
- sandbox_id: this._sandboxId,
331
- };
332
-
333
- logger.debug(`[Sandbox SDK] POST ${url}`);
334
- logger.debug(`[Sandbox SDK] Request body: ${JSON.stringify(data)}`);
335
-
336
- const response = await this._axios.post(url, data, { headers });
337
- logger.debug(`[Sandbox SDK] Response headers: ${JSON.stringify(response.headers)}`);
338
- logger.debug(`[Sandbox SDK] Response: ${JSON.stringify(response.data)}`);
339
-
340
- if (response.data.status !== 'Success') {
341
- throw new Error(`Failed to execute command: ${JSON.stringify(response.data)}`);
342
- }
343
-
344
- return response.data.result;
345
- }
346
-
347
- /**
348
- * Stop the sandbox
349
- */
350
- async stop() {
351
- if (!this._sandboxId) {
352
- logger.warn('No sandbox ID to stop');
353
- return;
354
- }
355
-
356
- try {
357
- const url = `${this._url}/stop`;
358
- const headers = this._buildHeaders();
359
- const data = {
360
- sandbox_id: this._sandboxId,
361
- };
362
-
363
- logger.debug(`[Sandbox SDK] POST ${url}`);
364
- logger.debug(`[Sandbox SDK] Request headers: ${JSON.stringify(headers)}`);
365
- logger.debug(`[Sandbox SDK] Request body: ${JSON.stringify(data)}`);
366
-
367
- const response = await this._axios.post(url, data, { headers });
368
- logger.debug(`[Sandbox SDK] Response headers: ${JSON.stringify(response.headers)}`);
369
- logger.debug(`[Sandbox SDK] Response: ${JSON.stringify(response.data)}`);
370
- logger.info(`Sandbox ${this._sandboxId} stopped successfully`);
371
- } catch (error) {
372
- logger.warn(`Failed to stop sandbox: ${error.message}`);
373
- }
374
- }
375
-
376
- /**
377
- * Create a bash session
378
- * @param {Object} options - Session options
379
- * @returns {Promise<Object>} Create session response
380
- */
381
- async createSession(options = {}) {
382
- const url = `${this._url}/create_session`;
383
- const headers = this._buildHeaders();
384
-
385
- const data = {
386
- sandbox_id: this._sandboxId,
387
- session: options.session || 'default',
388
- session_type: options.sessionType || 'bash',
389
- startup_source: options.startupSource || [],
390
- env_enable: options.envEnable || false,
391
- };
392
-
393
- if (options.env) {
394
- data.env = options.env;
395
- }
396
-
397
- if (options.startupTimeout !== undefined) {
398
- data.startup_timeout = options.startupTimeout;
399
- }
400
-
401
- logger.debug(`[Sandbox SDK] POST ${url}`);
402
- logger.debug(`[Sandbox SDK] Request headers: ${JSON.stringify(headers)}`);
403
- logger.debug(`[Sandbox SDK] Request body: ${JSON.stringify(data)}`);
404
-
405
- const response = await this._axios.post(url, data, { headers });
406
- logger.debug(`[Sandbox SDK] Response headers: ${JSON.stringify(response.headers)}`);
407
- logger.debug(`[Sandbox SDK] Response: ${JSON.stringify(response.data)}`);
408
-
409
- if (response.data.status !== 'Success') {
410
- throw new Error(`Failed to create session: ${JSON.stringify(response.data)}`);
411
- }
412
-
413
- return response.data.result;
414
- }
415
-
416
- /**
417
- * Run a command in a session
418
- * @param {Object} action - Action to execute
419
- * @returns {Promise<Object>} Observation result
420
- */
421
- async runInSession(action) {
422
- const url = `${this._url}/run_in_session`;
423
- const headers = this._buildHeaders();
424
-
425
- const data = {
426
- sandbox_id: this._sandboxId,
427
- command: action.command,
428
- session: action.session || 'default',
429
- timeout: action.timeout || null,
430
- is_interactive_command: action.isInteractiveCommand || false,
431
- check: action.check || 'raise',
432
- };
433
-
434
- if (action.expect) {
435
- data.expect = action.expect;
436
- }
437
-
438
- logger.debug(`[Sandbox SDK] POST ${url}`);
439
- logger.debug(`[Sandbox SDK] Request headers: ${JSON.stringify(headers)}`);
440
- logger.debug(`[Sandbox SDK] Request body: ${JSON.stringify(data)}`);
441
-
442
- const axiosConfig = {
443
- headers,
444
- };
445
-
446
- // Set axios timeout if provided (in milliseconds)
447
- // axiosTimeout takes precedence over timeout for HTTP layer
448
- if (action.axiosTimeout) {
449
- axiosConfig.timeout = action.axiosTimeout;
450
- } else if (action.timeout) {
451
- // If only backend timeout is set, also set axios timeout (timeout is in seconds)
452
- axiosConfig.timeout = action.timeout * 1000;
453
- }
454
-
455
- // Pass AbortSignal for cancellation support
456
- if (action.signal) {
457
- axiosConfig.signal = action.signal;
458
- }
459
-
460
- const response = await this._axios.post(url, data, axiosConfig);
461
- logger.debug(`[Sandbox SDK] Response headers: ${JSON.stringify(response.headers)}`);
462
- logger.debug(`[Sandbox SDK] Response: ${JSON.stringify(response.data)}`);
463
-
464
- if (response.data.status !== 'Success') {
465
- throw new Error(`Failed to run in session: ${JSON.stringify(response.data)}`);
466
- }
467
-
468
- return response.data.result;
469
- }
470
-
471
- /**
472
- * Write content to a file in the sandbox
473
- * @param {string} content - Content to write
474
- * @param {string} path - Target file path
475
- * @returns {Promise<Object>} Write file response
476
- */
477
- async writeFile(content, path) {
478
- const url = `${this._url}/write_file`;
479
- const headers = this._buildHeaders();
480
-
481
- const data = {
482
- content,
483
- path,
484
- sandbox_id: this._sandboxId,
485
- };
486
-
487
- logger.debug(`[Sandbox SDK] POST ${url}`);
488
- logger.debug(`[Sandbox SDK] Request headers: ${JSON.stringify(headers)}`);
489
- logger.debug(`[Sandbox SDK] Request body: ${JSON.stringify({ ...data, content: content.length > 100 ? content.substring(0, 100) + '...' : content })}`);
490
-
491
- const response = await this._axios.post(url, data, { headers });
492
- logger.debug(`[Sandbox SDK] Response: ${JSON.stringify(response.data)}`);
493
-
494
- if (response.data.status !== 'Success') {
495
- return {
496
- success: false,
497
- message: `Failed to write file ${path}: ${JSON.stringify(response.data)}`,
498
- };
499
- }
500
-
501
- return {
502
- success: true,
503
- message: `Successfully wrote content to file ${path}`,
504
- };
505
- }
506
-
507
- /**
508
- * Read a file from the sandbox
509
- * @param {string} filePath - File path to read
510
- * @param {number} startLine - Start line (1-indexed)
511
- * @param {number} endLine - End line (1-indexed)
512
- * @returns {Promise<Object>} Read file response
513
- */
514
- async readFile(filePath, startLine, endLine) {
515
- if (startLine < 1 || endLine < startLine) {
516
- throw new Error(`Invalid line range: startLine=${startLine}, endLine=${endLine}`);
517
- }
518
-
519
- if (endLine - startLine > 1000) {
520
- throw new Error(`Line range too large: ${endLine - startLine} lines (max 1000)`);
521
- }
522
-
523
- const command = `sed -n ${startLine},${endLine}p ${filePath}`;
524
- const result = await this.execute(command);
525
-
526
- if (result.exit_code !== 0) {
527
- throw new Error(`Failed to read file ${filePath}: ${result.stderr}`);
528
- }
529
-
530
- return {
531
- content: result.stdout,
532
- };
533
- }
534
-
535
- /**
536
- * Upload a file to the sandbox
537
- * @param {string} localPath - Local file path
538
- * @param {string} targetPath - Target path in sandbox
539
- * @returns {Promise<Object>} Upload response
540
- */
541
- async uploadFile(localPath, targetPath) {
542
- const fs = require('fs');
543
- const path = require('path');
544
-
545
- try {
546
- if (!fs.existsSync(localPath)) {
547
- return {
548
- success: false,
549
- message: `File not found: ${localPath}`,
550
- };
551
- }
552
-
553
- const url = `${this._url}/upload`;
554
- const headers = this._buildHeaders();
555
- delete headers['Content-Type'];
556
-
557
- // Use native FormData for bun compatibility
558
- const form = new FormData();
559
- const fileBuffer = fs.readFileSync(localPath);
560
- const blob = new Blob([fileBuffer], { type: 'application/octet-stream' });
561
-
562
- form.append('file', blob, path.basename(localPath));
563
- form.append('target_path', targetPath);
564
- form.append('sandbox_id', this._sandboxId);
565
- form.append('container_name', this._sandboxId);
566
-
567
- logger.debug(`[Sandbox SDK] POST ${url}`);
568
- logger.debug(`[Sandbox SDK] Request body: target_path=${targetPath}, sandbox_id=${this._sandboxId}, container_name=${this._sandboxId}, file=${path.basename(localPath)}`);
569
- logger.debug(`[Sandbox SDK] Upload file: ${localPath} -> ${targetPath}`);
570
-
571
- const response = await fetch(url, {
572
- method: 'POST',
573
- headers,
574
- body: form,
575
- });
576
-
577
- const data = await response.json();
578
- logger.debug(`[Sandbox SDK] Response: ${JSON.stringify(data)}`);
579
-
580
- if (data.status !== 'Success') {
581
- return {
582
- success: false,
583
- message: `Failed to upload file: ${JSON.stringify(data)}`,
584
- };
585
- }
586
-
587
- return {
588
- success: true,
589
- message: `Successfully uploaded ${path.basename(localPath)} to ${targetPath}`,
590
- };
591
- } catch (error) {
592
- logger.debug(`[Sandbox SDK] Upload error: ${error}`);
593
- const errorMessage = error?.message || (typeof error === 'string' ? error : 'Unknown error');
594
- return {
595
- success: false,
596
- message: `Upload failed: ${errorMessage}`,
597
- };
598
- }
599
- }
600
-
601
- /**
602
- * Download a file from the sandbox
603
- * @param {string} filePath - File path in sandbox
604
- * @returns {Promise<Object>} Download response
605
- */
606
- async downloadFile(filePath) {
607
- const url = `${this._url}/read_file`;
608
- const headers = this._buildHeaders();
609
-
610
- const data = {
611
- path: filePath,
612
- sandbox_id: this._sandboxId,
613
- };
614
-
615
- logger.debug(`[Sandbox SDK] POST ${url}`);
616
- logger.debug(`[Sandbox SDK] Request body: ${JSON.stringify(data)}`);
617
-
618
- const response = await this._axios.post(url, data, { headers });
619
- logger.debug(`[Sandbox SDK] Response: ${JSON.stringify(response.data)}`);
620
-
621
- if (response.data.status !== 'Success') {
622
- throw new Error(`Failed to download file: ${JSON.stringify(response.data)}`);
623
- }
624
-
625
- return response.data.result;
626
- }
627
-
628
- /**
629
- * Make a generic HTTP request to the sandbox API
630
- * @param {Object} request - Request object
631
- * @param {string} request.method - HTTP method (GET, POST, PUT, DELETE, etc.)
632
- * @param {string} request.uri - Request URI path
633
- * @param {Object} request.requestBody - Request body data
634
- * @param {Object} request.headers - Request headers
635
- * @returns {Promise<Object>} Response data
636
- */
637
- async makeRequest(request) {
638
- const { method, uri, requestBody, headers } = request;
639
- const url = `${this.config.baseUrl}${uri}`;
640
- const requestHeaders = this._buildHeaders();
641
-
642
- // Merge custom headers
643
- if (headers) {
644
- Object.assign(requestHeaders, headers);
645
- }
646
-
647
- logger.debug(`[Sandbox SDK] ${method} ${url}`);
648
- logger.debug(`[Sandbox SDK] Request headers: ${JSON.stringify(requestHeaders)}`);
649
- if (requestBody) {
650
- logger.debug(`[Sandbox SDK] Request body: ${JSON.stringify(requestBody)}`);
651
- }
652
-
653
- const axiosConfig = {
654
- method: method.toLowerCase(),
655
- url,
656
- headers: requestHeaders,
657
- };
658
-
659
- if (requestBody) {
660
- axiosConfig.data = requestBody;
661
- }
662
-
663
- const response = await axios(axiosConfig);
664
- logger.debug(`[Sandbox SDK] Response: ${JSON.stringify(response.data)}`);
665
-
666
- return {
667
- data: response.data,
668
- headers: response.headers,
669
- status: response.status
670
- };
671
- }
672
-
673
- /**
674
- * Close a session
675
- * @param {string} session - Session name (default: 'default')
676
- * @returns {Promise<Object>} Close session response
677
- */
678
- async closeSession(session = 'default') {
679
- const url = `${this._url}/close_session`;
680
- const headers = this._buildHeaders();
681
-
682
- const data = {
683
- sandbox_id: this._sandboxId,
684
- session: session,
685
- };
686
-
687
- logger.debug(`[Sandbox SDK] POST ${url}`);
688
- logger.debug(`[Sandbox SDK] Request headers: ${JSON.stringify(headers)}`);
689
- logger.debug(`[Sandbox SDK] Request body: ${JSON.stringify(data)}`);
690
-
691
- const response = await this._axios.post(url, data, { headers });
692
- logger.debug(`[Sandbox SDK] Response headers: ${JSON.stringify(response.headers)}`);
693
- logger.debug(`[Sandbox SDK] Response: ${JSON.stringify(response.data)}`);
694
-
695
- if (response.data.status !== 'Success') {
696
- throw new Error(`Failed to close session: ${JSON.stringify(response.data)}`);
697
- }
698
-
699
- return response.data.result;
700
- }
701
-
702
- /**
703
- * Run a command asynchronously in a session using nohup + kill -0 polling
704
- * @param {Object} action - Action configuration
705
- * @param {string} action.session - Session name
706
- * @param {string} action.command - Command to execute
707
- * @param {number} [action.timeout] - Total timeout in milliseconds
708
- * @param {number} [action.pollInterval] - Polling interval in milliseconds (default: 5000)
709
- * @param {boolean} [action.killOnTimeout] - Whether to kill process on timeout (default: false)
710
- * @param {boolean} [action.cleanupLog] - Whether to cleanup log file (default: false)
711
- * @returns {Promise<Object>} Result with output, exit_code, and optional timeout/warning
712
- */
713
- async runInSessionAsync(action) {
714
- const session = action.session || 'default';
715
- const timeoutMs = action.timeout || 3600000; // Default 1 hour
716
- const pollInterval = action.pollInterval || 5000;
717
- const killOnTimeout = action.killOnTimeout || false;
718
- const cleanupLog = action.cleanupLog || false;
719
-
720
- // Generate unique log file
721
- const logFile = `/tmp/arun-${Date.now()}.log`;
722
-
723
- // Build nohup command with PID marker
724
- const nohupCommand = `nohup ${action.command} < /dev/null > ${logFile} 2>&1 & echo PIDSTART$!PIDEND;disown`;
725
-
726
- logger.debug(`[Sandbox SDK] Starting async command: ${action.command}`);
727
-
728
- // 1. Execute nohup command
729
- const startResult = await this.runInSession({
730
- session,
731
- command: nohupCommand
732
- });
733
-
734
- // 2. Extract PID
735
- const pidMatch = startResult.output.match(/PIDSTART(\d+)PIDEND/);
736
- if (!pidMatch) {
737
- throw new Error('Failed to extract PID from nohup output');
738
- }
739
- const pid = pidMatch[1];
740
- logger.debug(`[Sandbox SDK] Started process with PID: ${pid}`);
741
-
742
- // 3. Poll for process completion
743
- const startTime = Date.now();
744
- let pollCount = 0;
745
- const maxPolls = Math.ceil(timeoutMs / pollInterval);
746
-
747
- while (Date.now() - startTime < timeoutMs) {
748
- pollCount++;
749
- logger.debug(`[Sandbox SDK] [${pollCount}/${maxPolls}] Checking process ${pid}...`);
750
- console.log(`[${pollCount}/${maxPolls}] Waiting for process ${pid}...`);
751
-
752
- // Check if process is still running (kill -0)
753
- const checkResult = await this.runInSession({
754
- session,
755
- command: `kill -0 ${pid}`,
756
- check: 'silent' // Don't throw on non-zero exit
757
- });
758
-
759
- if (checkResult.exit_code !== 0) {
760
- // Process has finished
761
- logger.debug(`[Sandbox SDK] Process ${pid} finished`);
762
- console.log(`Process ${pid} finished`);
763
- break;
764
- }
765
-
766
- // Wait before next poll
767
- await new Promise(resolve => setTimeout(resolve, pollInterval));
768
- }
769
-
770
- // 4. Check for timeout
771
- const elapsed = Date.now() - startTime;
772
- let timedOut = false;
773
-
774
- if (elapsed >= timeoutMs) {
775
- timedOut = true;
776
- logger.warn(`[Sandbox SDK] Process ${pid} timed out after ${elapsed}ms`);
777
- console.log(`⚠️ Process ${pid} timed out after ${elapsed}ms`);
778
-
779
- if (killOnTimeout) {
780
- logger.debug(`[Sandbox SDK] Killing process ${pid}`);
781
- await this.runInSession({
782
- session,
783
- command: `kill ${pid}`,
784
- check: 'silent'
785
- });
786
- // Wait a moment for process to terminate
787
- await new Promise(resolve => setTimeout(resolve, 100));
788
- }
789
- }
790
-
791
- // 5. Read output from log file
792
- const outputResult = await this.runInSession({
793
- session,
794
- command: `cat ${logFile} 2>/dev/null; echo "EXITCODE:$?"`
795
- });
796
-
797
- // Parse exit code from output
798
- let output = outputResult.output || '';
799
- let exitCode = 0;
800
-
801
- // If process was killed due to timeout, exit code is 137 (SIGKILL)
802
- if (timedOut && killOnTimeout) {
803
- exitCode = 137;
804
- } else {
805
- const exitCodeMatch = output.match(/EXITCODE:(\d+)/);
806
- if (exitCodeMatch) {
807
- exitCode = parseInt(exitCodeMatch[1], 10);
808
- }
809
- }
810
-
811
- output = output.replace(/EXITCODE:\d+\n?$/, '');
812
-
813
- // 6. Cleanup log file if requested
814
- if (cleanupLog) {
815
- await this.runInSession({
816
- session,
817
- command: `rm -f ${logFile}`,
818
- check: 'silent'
819
- });
820
- logger.debug(`[Sandbox SDK] Cleaned up log file: ${logFile}`);
821
- }
822
-
823
- // Build result
824
- const result = {
825
- output: output.trim(),
826
- exit_code: exitCode
827
- };
828
-
829
- if (timedOut) {
830
- result.timeout = true;
831
- result.warning = `Process ${pid} timed out after ${timeoutMs}ms`;
832
- }
833
-
834
- return result;
835
- }
836
-
837
- /**
838
- * Close the sandbox
839
- */
840
- async close() {
841
- await this.stop();
842
- }
843
- }
844
-
845
- module.exports = { SandboxClient };