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,1393 +0,0 @@
1
- const { SandboxClient, SandboxConfig } = require('../utils/sandbox-client');
2
- const fs = require('fs-extra');
3
- const path = require('path');
4
- const logger = require('../utils/logger');
5
- const sessionCommand = require('./sandbox/session');
6
- const { gracefulExit } = require('../utils/execution_logger');
7
- const configManager = require('../utils/configManager');
8
- const { printNextStep, TIPS } = require('../utils/tips');
9
-
10
- const isOpenSource = process.env.ROCKCLI_MODE === 'opensource';
11
- const DEFAULT_OPEN_SOURCE_IMAGE = 'python:3.11';
12
-
13
- // 配置文件路径
14
- const CONFIG_DIR = path.join(process.env.HOME || process.env.USERPROFILE, '.rock');
15
- const CONFIG_FILE = path.join(CONFIG_DIR, 'settings.json');
16
-
17
- /**
18
- * 读取配置文件中的 sandbox 配置
19
- * @param {Object} argv - Command line arguments
20
- * @returns {Object} - Sandbox config with proper priority for all parameters
21
- */
22
- function readSandboxConfig(argv = {}) {
23
- const config = configManager.readConfig();
24
- const sandboxConfig = config?.sandbox || {};
25
-
26
- // Get default image from internal config or use open source default
27
- let defaultImage;
28
- if (isOpenSource) {
29
- defaultImage = DEFAULT_OPEN_SOURCE_IMAGE;
30
- } else {
31
- const { getInternalSandboxConfig } = require('../sdks/sandbox/core/config');
32
- const internalDefaults = getInternalSandboxConfig();
33
- defaultImage = internalDefaults.image;
34
- }
35
-
36
- return {
37
- base_url: argv.baseUrl || sandboxConfig.base_url || 'http://127.0.0.1:8080',
38
- image: defaultImage,
39
- api_key: configManager.getApiKeyWithPriority(argv),
40
- cluster: configManager.getClusterWithPriority(argv),
41
- user_id: configManager.getUserIdWithPriority(argv),
42
- experiment_id: configManager.getExperimentIdWithPriority(argv),
43
- };
44
- }
45
-
46
- // Sandbox command module for yargs
47
- module.exports = {
48
- command: 'sandbox [action]',
49
- describe: 'Sandbox operations:\n' +
50
- ' start 启动一个新的 sandbox 实例\n' +
51
- ' stop 停止指定的 sandbox 实例\n' +
52
- ' execute 在 sandbox 中执行命令 (别名: exec)\n' +
53
- ' exec 在 sandbox 中执行命令 (别名)\n' +
54
- ' status 查看 sandbox 的运行状态\n' +
55
- ' upload 上传文件或文件夹到 sandbox\n' +
56
- ' download 从 sandbox 下载文件\n' +
57
- ' write-file 在 sandbox 中写入文件\n' +
58
- ' read-file 从 sandbox 读取文件\n' +
59
- ' session Session 管理 (create, run, close)\n' +
60
- ' replay 回放请求列表 (从 stdin 读取 JSON)\n' +
61
- ' build 构建 Docker 镜像(已废弃)\n' +
62
- ' push 推送 Docker 镜像(已废弃)\n' +
63
- ' run 运行容器(已废弃)',
64
- builder: (yargs) => {
65
- return yargs
66
- .positional('action', {
67
- describe: 'Sandbox action to perform, or a shell command to execute',
68
- type: 'string',
69
- })
70
- .option('api-key', {
71
- describe: 'API key for authentication',
72
- type: 'string',
73
- group: 'Command Options:'
74
- })
75
- .option('base-url', {
76
- describe: 'Base URL for sandbox service',
77
- type: 'string',
78
- group: 'Command Options:'
79
- })
80
- .option('sandbox-id', {
81
- alias: 'id',
82
- describe: 'Sandbox ID (for operations on existing sandbox)',
83
- type: 'string',
84
- group: 'Command Options:'
85
- })
86
- .option('image', {
87
- describe: 'Docker image to use',
88
- type: 'string',
89
- group: 'Command Options:'
90
- })
91
- .option('memory', {
92
- describe: 'Memory limit',
93
- type: 'string',
94
- default: '8g',
95
- group: 'Command Options:'
96
- })
97
- .option('cpus', {
98
- describe: 'CPU limit',
99
- type: 'number',
100
- default: 2.0,
101
- group: 'Command Options:'
102
- })
103
- .option('timeout', {
104
- describe: 'Startup timeout in seconds',
105
- type: 'number',
106
- default: 120,
107
- group: 'Command Options:'
108
- })
109
- .option('auto-clear', {
110
- describe: 'Auto clear time in seconds',
111
- type: 'number',
112
- default: 300,
113
- group: 'Command Options:'
114
- })
115
- .option('cluster', {
116
- describe: 'Cluster ID (sets X-Cluster header)',
117
- type: 'string',
118
- group: 'Command Options:'
119
- })
120
- .option('experiment-id', {
121
- describe: 'Experiment ID (sets X-Experiment-Id header)',
122
- type: 'string',
123
- group: 'Command Options:'
124
- })
125
- .option('user-id', {
126
- describe: 'User ID (sets X-User-Id header)',
127
- type: 'string',
128
- group: 'Command Options:'
129
- })
130
- .option('command', {
131
- describe: 'Command to execute',
132
- type: 'string',
133
- group: 'Command Options:'
134
- })
135
- .option('file', {
136
- describe: 'File path for upload/download/read/write',
137
- type: 'string',
138
- group: 'Command Options:'
139
- })
140
- .option('dir', {
141
- alias: 'd',
142
- describe: 'Local directory path for upload (mutually exclusive with --file)',
143
- type: 'string',
144
- group: 'Command Options:'
145
- })
146
- .option('recursive', {
147
- alias: 'r',
148
- describe: 'Recursively upload subdirectories',
149
- type: 'boolean',
150
- default: false,
151
- group: 'Command Options:'
152
- })
153
- .option('all', {
154
- alias: 'a',
155
- describe: 'Include hidden files (starting with .)',
156
- type: 'boolean',
157
- default: false,
158
- group: 'Command Options:'
159
- })
160
- .option('clobber', {
161
- alias: 'c',
162
- describe: 'Overwrite existing files',
163
- type: 'boolean',
164
- default: false,
165
- group: 'Command Options:'
166
- })
167
- .option('noclobber', {
168
- alias: 'n',
169
- describe: 'Do not overwrite existing files',
170
- type: 'boolean',
171
- default: false,
172
- group: 'Command Options:'
173
- })
174
- .option('interactive', {
175
- alias: 'i',
176
- describe: 'Prompt before overwrite',
177
- type: 'boolean',
178
- default: false,
179
- group: 'Command Options:'
180
- })
181
- .option('target-path', {
182
- describe: 'Target path for upload/write',
183
- type: 'string',
184
- group: 'Command Options:'
185
- })
186
- .option('start-line', {
187
- describe: 'Start line for read-file (1-indexed)',
188
- type: 'number',
189
- group: 'Command Options:'
190
- })
191
- .option('end-line', {
192
- describe: 'End line for read-file (1-indexed)',
193
- type: 'number',
194
- group: 'Command Options:'
195
- })
196
- .option('content', {
197
- describe: 'Content to write to file',
198
- type: 'string',
199
- group: 'Command Options:'
200
- })
201
- .option('extra-header', {
202
- alias: 'H',
203
- type: 'array',
204
- description: 'Extra HTTP headers in format "Key=Value". Can be used multiple times.',
205
- group: 'Command Options:',
206
- coerce: (headers) => {
207
- const result = {};
208
- if (headers) {
209
- headers.forEach(header => {
210
- if (header.includes('=')) {
211
- const [key, value] = header.split('=', 2);
212
- result[key.trim()] = value.trim();
213
- } else {
214
- logger.warn(`Invalid header format: ${header}. Expected format: 'Key=Value'`);
215
- }
216
- });
217
- }
218
- return result;
219
- }
220
- })
221
- .option('wait-for-alive', {
222
- describe: 'Wait for sandbox to be alive before returning',
223
- type: 'boolean',
224
- default: false,
225
- group: 'Command Options:'
226
- })
227
- .option('stop', {
228
- describe: 'Execute stop request during replay (default: false)',
229
- type: 'boolean',
230
- default: false,
231
- group: 'Command Options:'
232
- })
233
- .option('interval', {
234
- describe: 'Interval between requests in seconds (default: 5, use 0 to disable waiting)',
235
- type: 'number',
236
- default: 5,
237
- group: 'Command Options:'
238
- })
239
- .option('log-file', {
240
- describe: 'Log file path to record replay details (default: replay.log)',
241
- type: 'string',
242
- default: 'replay.log',
243
- group: 'Command Options:'
244
- })
245
- .option('raw', {
246
- describe: 'Return raw JSON output instead of formatted text',
247
- type: 'boolean',
248
- default: false,
249
- group: 'Command Options:'
250
- })
251
- .command(sessionCommand)
252
- .example([
253
- ['$0 --id <id> "ls -la"', 'Execute a command in sandbox'],
254
- ['$0 --id <id> status', 'Get sandbox status'],
255
- ['$0 --id <id> log', 'View sandbox logs'],
256
- ['$0 attach <id>', 'Attach to sandbox (interactive REPL)'],
257
- ['$0 sandbox start --image python:3.11', 'Start a new sandbox'],
258
- ['$0 sandbox stop --id <id>', 'Stop a sandbox'],
259
- ]);
260
- },
261
- handler: async (argv) => {
262
- try {
263
- // If no action is provided, show help
264
- if (!argv.action) {
265
- console.error('Error: action is required');
266
- console.error('Use "rock-cli sandbox --help" for more information.');
267
- gracefulExit(1);
268
- }
269
-
270
- // Known sandbox actions
271
- const knownActions = ['start', 'stop', 'execute', 'exec', 'status', 'upload',
272
- 'download', 'write-file', 'read-file', 'replay', 'session',
273
- 'build', 'push', 'run'];
274
-
275
- // If action is provided and not a known action, it's either:
276
- // 1. 'log' - show logs
277
- // 2. A shell command to execute
278
- if (argv.action && !knownActions.includes(argv.action)) {
279
- if (argv.action === 'log') {
280
- // Show sandbox logs
281
- return handleLogView(argv);
282
- } else {
283
- // Treat as shell command
284
- argv.command = argv.action;
285
- return handleExecute(argv);
286
- }
287
- }
288
-
289
- switch (argv.action) {
290
- case 'start':
291
- await handleStart(argv);
292
- break;
293
- case 'stop':
294
- await handleStop(argv);
295
- break;
296
- case 'execute':
297
- case 'exec':
298
- await handleExecute(argv);
299
- break;
300
- case 'status':
301
- await handleStatus(argv);
302
- break;
303
- case 'upload':
304
- await handleUpload(argv);
305
- break;
306
- case 'download':
307
- await handleDownload(argv);
308
- break;
309
- case 'write-file':
310
- await handleWriteFile(argv);
311
- break;
312
- case 'read-file':
313
- await handleReadFile(argv);
314
- break;
315
- case 'replay':
316
- await handleReplay(argv);
317
- break;
318
- case 'build':
319
- case 'push':
320
- case 'run':
321
- await handleLegacyActions(argv);
322
- break;
323
- default:
324
- console.error(`Unknown sandbox action: ${argv.action}`);
325
- gracefulExit(1);
326
- }
327
- } catch (error) {
328
- // Check for 401 authentication errors
329
- if (error.response && error.response.status === 401) {
330
- logger.error('Authentication failed: Invalid or missing API key');
331
- console.error('\n💡 Please configure your API key:');
332
- console.error(' Option 1: export ROCK_API_KEY=<your-key>');
333
- console.error(' Option 2: Use --api-key flag: rc sandbox start --api-key <your-key>\n');
334
- } else {
335
- logger.error(`Sandbox action failed: ${error.message}`);
336
- }
337
- gracefulExit(1);
338
- }
339
- },
340
- };
341
-
342
- /**
343
- * Handle start action
344
- */
345
- async function handleStart(argv) {
346
- const sandboxConfig = readSandboxConfig(argv);
347
- const baseUrl = argv.baseUrl || sandboxConfig.base_url;
348
- const apiKey = argv.apiKey || sandboxConfig.api_key;
349
- const cluster = argv.cluster || sandboxConfig.cluster;
350
- const userId = argv.userId || sandboxConfig.user_id;
351
- const experimentId = argv.experimentId || sandboxConfig.experiment_id;
352
- const image = argv.image || sandboxConfig.image;
353
-
354
- if (!baseUrl) {
355
- throw new Error('base-url is required for start action (use --base-url or run "rock-cli login" first)');
356
- }
357
-
358
- const config = new SandboxConfig({
359
- baseUrl: baseUrl,
360
- xrlAuthorization: apiKey,
361
- image: image,
362
- memory: argv.memory,
363
- cpus: argv.cpus,
364
- startupTimeout: argv.timeout,
365
- autoClearSeconds: argv.autoClear,
366
- cluster: cluster,
367
- userId: userId,
368
- experimentId: experimentId,
369
- extraHeaders: argv.extraHeader || {},
370
- waitForAlive: argv.waitForAlive,
371
- });
372
-
373
- const client = new SandboxClient(config);
374
-
375
- if (!argv.raw) {
376
- console.log(`Starting sandbox with image: ${config.image}...`);
377
- }
378
-
379
- const result = await client.start();
380
-
381
- if (argv.raw) {
382
- // Output raw JSON
383
- console.log(JSON.stringify({
384
- sandboxId: result.sandboxId,
385
- hostName: result.hostName,
386
- hostIp: result.hostIp,
387
- isAlive: result.isAlive,
388
- success: true
389
- }));
390
- } else {
391
- console.log('✅ Sandbox started successfully!');
392
- console.log(` - Sandbox ID: ${result.sandboxId}`);
393
- console.log(` - Host Name: ${result.hostName}`);
394
- console.log(` - Host IP: ${result.hostIp}`);
395
-
396
- if (argv.waitForAlive) {
397
- console.log(` - Status: ${result.isAlive ? '🟢 Ready' : '🟡 Starting...'}`);
398
- }
399
-
400
- // Show tip for next step
401
- const tip = TIPS.afterStart(result.sandboxId);
402
- printNextStep(tip.message, tip.command);
403
- }
404
- }
405
-
406
- /**
407
- * Handle stop action
408
- */
409
- async function handleStop(argv) {
410
- if (!argv.sandboxId) {
411
- throw new Error('sandbox-id is required for stop action');
412
- }
413
-
414
- const sandboxConfig = readSandboxConfig(argv);
415
- const baseUrl = argv.baseUrl || sandboxConfig.base_url;
416
- const apiKey = argv.apiKey || sandboxConfig.api_key;
417
- const cluster = argv.cluster || sandboxConfig.cluster;
418
- const userId = argv.userId || sandboxConfig.user_id;
419
- const experimentId = argv.experimentId || sandboxConfig.experiment_id;
420
-
421
- if (!baseUrl) {
422
- throw new Error('base-url is required for stop action (use --base-url or run "rock-cli login" first)');
423
- }
424
-
425
- const config = new SandboxConfig({
426
- baseUrl: baseUrl,
427
- xrlAuthorization: apiKey,
428
- cluster: cluster,
429
- userId: userId,
430
- experimentId: experimentId,
431
- extraHeaders: argv.extraHeader || {},
432
- });
433
-
434
- const client = new SandboxClient(config, { requireImage: false });
435
- client._sandboxId = argv.sandboxId;
436
-
437
- console.log(`Stopping sandbox: ${argv.sandboxId}...`);
438
- await client.stop();
439
- console.log('✅ Sandbox stopped successfully!');
440
- }
441
-
442
- /**
443
- * Handle execute action
444
- */
445
- async function handleExecute(argv) {
446
- // Get command from --command option or from process.argv after --
447
- let command = argv.command;
448
- if (!command) {
449
- const separatorIndex = process.argv.indexOf('--');
450
- if (separatorIndex !== -1 && separatorIndex < process.argv.length - 1) {
451
- command = process.argv.slice(separatorIndex + 1).join(' ');
452
- }
453
- }
454
-
455
- if (!command) {
456
- throw new Error('command is required. Use --command "your command" or: rock-cli sandbox execute --sandbox-id <id> --base-url <url> -- ps -aux');
457
- }
458
-
459
- if (!argv.sandboxId) {
460
- throw new Error('sandbox-id is required for execute action');
461
- }
462
-
463
- const sandboxConfig = readSandboxConfig(argv);
464
- const baseUrl = argv.baseUrl || sandboxConfig.base_url;
465
- const apiKey = argv.apiKey || sandboxConfig.api_key;
466
- const cluster = argv.cluster || sandboxConfig.cluster;
467
- const userId = argv.userId || sandboxConfig.user_id;
468
- const experimentId = argv.experimentId || sandboxConfig.experiment_id;
469
-
470
- if (!baseUrl) {
471
- throw new Error('base-url is required for execute action (use --base-url or run "rock-cli login" first)');
472
- }
473
-
474
- const config = new SandboxConfig({
475
- baseUrl: baseUrl,
476
- xrlAuthorization: apiKey,
477
- cluster: cluster,
478
- userId: userId,
479
- experimentId: experimentId,
480
- extraHeaders: argv.extraHeader || {},
481
- });
482
-
483
- const client = new SandboxClient(config, { requireImage: false });
484
- client._sandboxId = argv.sandboxId;
485
-
486
- console.log(`Executing command: ${command}`);
487
- const result = await client.execute(command);
488
-
489
- if (result.stdout) {
490
- console.log('Output:');
491
- console.log(result.stdout);
492
- }
493
-
494
- if (result.stderr) {
495
- console.error('Errors:');
496
- console.error(result.stderr);
497
- }
498
-
499
- console.log(`Exit code: ${result.exit_code}`);
500
-
501
- if (result.exit_code !== 0) {
502
- gracefulExit(1);
503
- }
504
- }
505
-
506
- /**
507
- * Handle status action
508
- */
509
- async function handleStatus(argv) {
510
- if (!argv.sandboxId) {
511
- throw new Error('sandbox-id is required for status action');
512
- }
513
-
514
- const sandboxConfig = readSandboxConfig(argv);
515
- const baseUrl = argv.baseUrl || sandboxConfig.base_url;
516
- const apiKey = argv.apiKey || sandboxConfig.api_key;
517
- const cluster = argv.cluster || sandboxConfig.cluster;
518
- const userId = argv.userId || sandboxConfig.user_id;
519
- const experimentId = argv.experimentId || sandboxConfig.experiment_id;
520
-
521
- if (!baseUrl) {
522
- throw new Error('base-url is required for status action (use --base-url or run "rock-cli login" first)');
523
- }
524
-
525
- const config = new SandboxConfig({
526
- baseUrl: baseUrl,
527
- xrlAuthorization: apiKey,
528
- cluster: cluster,
529
- userId: userId,
530
- experimentId: experimentId,
531
- extraHeaders: argv.extraHeader || {},
532
- });
533
-
534
- const client = new SandboxClient(config, { requireImage: false });
535
- client._sandboxId = argv.sandboxId;
536
-
537
- console.log(`Getting status for sandbox: ${argv.sandboxId}...`);
538
- const status = await client.getStatus();
539
-
540
- console.log('📊 Sandbox Status:');
541
- console.log(` - Sandbox ID: ${status.sandboxId}`);
542
- console.log(` - Cluster: ${status.cluster || 'N/A'}`);
543
- console.log(` - Host Name: ${status.hostName || 'N/A'}`);
544
- console.log(` - Host IP: ${status.hostIp || 'N/A'}`);
545
- console.log(` - Alive: ${status.isAlive ? '🟢 Yes' : '🔴 No'}`);
546
- console.log(` - Image: ${status.image || 'N/A'}`);
547
- console.log(` - CPUs: ${status.cpus || 'N/A'}`);
548
- console.log(` - Memory: ${status.memory || 'N/A'}`);
549
- console.log(` - Namespace: ${status.namespace || 'N/A'}`);
550
- console.log(` - Request ID: ${status.requestId || 'N/A'}`);
551
- console.log(` - EagleEye Trace ID: ${status.eagleeyeTraceid || 'N/A'}`);
552
-
553
- if (status.portMapping && Object.keys(status.portMapping).length > 0) {
554
- console.log(` - Port Mappings:`);
555
- for (const [containerPort, hostPort] of Object.entries(status.portMapping)) {
556
- console.log(` - ${containerPort} -> ${hostPort}`);
557
- }
558
- }
559
-
560
- if (status.status) {
561
- console.log(` - Stage Status:`);
562
- for (const [stage, details] of Object.entries(status.status)) {
563
- const icon = details.status === 'success' ? '✅' : details.status === 'failed' ? '❌' : '⏳';
564
- console.log(` - ${icon} ${stage}: ${details.status}`);
565
- if (details.message) {
566
- console.log(` ${details.message}`);
567
- }
568
- }
569
- }
570
- }
571
-
572
- /**
573
- * Handle upload action
574
- */
575
- async function handleUpload(argv) {
576
- // Check mutual exclusivity
577
- if (argv.file && argv.dir) {
578
- throw new Error('--file and --dir are mutually exclusive');
579
- }
580
-
581
- if (!argv.file && !argv.dir) {
582
- throw new Error('--file or --dir is required for upload action');
583
- }
584
-
585
- if (!argv.targetPath) {
586
- throw new Error('target-path is required for upload action');
587
- }
588
-
589
- if (!argv.sandboxId) {
590
- throw new Error('sandbox-id is required for upload action');
591
- }
592
-
593
- const sandboxConfig = readSandboxConfig(argv);
594
- const baseUrl = argv.baseUrl || sandboxConfig.base_url;
595
- const apiKey = argv.apiKey || sandboxConfig.api_key;
596
- const cluster = argv.cluster || sandboxConfig.cluster;
597
- const userId = argv.userId || sandboxConfig.user_id;
598
- const experimentId = argv.experimentId || sandboxConfig.experiment_id;
599
-
600
- if (!baseUrl) {
601
- throw new Error('base-url is required for upload action (use --base-url or run "rock-cli login" first)');
602
- }
603
-
604
- const config = new SandboxConfig({
605
- baseUrl: baseUrl,
606
- xrlAuthorization: apiKey,
607
- cluster: cluster,
608
- userId: userId,
609
- experimentId: experimentId,
610
- extraHeaders: argv.extraHeader || {},
611
- });
612
-
613
- const client = new SandboxClient(config, { requireImage: false });
614
- client._sandboxId = argv.sandboxId;
615
-
616
- // Branch: directory upload or single file upload
617
- if (argv.dir) {
618
- await handleDirectoryUpload(argv, client);
619
- } else {
620
- console.log(`Uploading ${argv.file} to ${argv.targetPath}...`);
621
- const result = await client.uploadFile(argv.file, argv.targetPath);
622
-
623
- if (result.success) {
624
- console.log('Upload successful!');
625
- console.log(` ${result.message}`);
626
- } else {
627
- console.error('Upload failed!');
628
- console.error(` ${result.message}`);
629
- gracefulExit(1);
630
- }
631
- }
632
- }
633
-
634
- /**
635
- * Escape string for shell command
636
- */
637
- function shellEscape(str) {
638
- return `'${str.replace(/'/g, "'\\''")}'`;
639
- }
640
-
641
- /**
642
- * Collect files from a local directory
643
- */
644
- function collectFiles(dir, options) {
645
- const { recursive, includeHidden } = options;
646
- const files = [];
647
-
648
- function walk(currentDir, relativePath) {
649
- const entries = fs.readdirSync(currentDir, { withFileTypes: true });
650
-
651
- for (const entry of entries) {
652
- // Filter hidden files
653
- if (!includeHidden && entry.name.startsWith('.')) {
654
- continue;
655
- }
656
-
657
- // Skip symbolic links
658
- if (entry.isSymbolicLink()) {
659
- logger.debug(`Skipping symlink: ${path.join(currentDir, entry.name)}`);
660
- continue;
661
- }
662
-
663
- const fullPath = path.join(currentDir, entry.name);
664
- const relPath = relativePath ? path.join(relativePath, entry.name) : entry.name;
665
-
666
- if (entry.isFile()) {
667
- files.push({ localPath: fullPath, relativePath: relPath });
668
- } else if (entry.isDirectory() && recursive) {
669
- walk(fullPath, relPath);
670
- }
671
- }
672
- }
673
-
674
- walk(dir, '');
675
- return files;
676
- }
677
-
678
- /**
679
- * Get list of existing files in remote directory
680
- */
681
- async function getRemoteFileList(client, targetPath, recursive) {
682
- const normalizedPath = targetPath.replace(/\/+$/, '');
683
-
684
- try {
685
- // Use simple ls or find command, handle errors gracefully
686
- let result;
687
- if (recursive) {
688
- // For recursive, use find
689
- result = await client.execute(['find', normalizedPath, '-type', 'f']);
690
- } else {
691
- // For non-recursive, use ls -1A to include hidden files (but exclude . and ..)
692
- result = await client.execute(['ls', '-1A', normalizedPath]);
693
- }
694
-
695
- const files = new Set();
696
- if (result.exit_code === 0 && result.stdout) {
697
- String(result.stdout).split('\n').filter(Boolean).forEach(f => {
698
- // find returns full path, ls returns filename only
699
- const fullPath = recursive ? f : path.posix.join(normalizedPath, f);
700
- files.add(fullPath);
701
- });
702
- }
703
- logger.debug(`getRemoteFileList: path=${targetPath}, recursive=${recursive}, found=${files.size} files`);
704
- return files;
705
- } catch (error) {
706
- // Return empty set if directory doesn't exist
707
- logger.debug(`getRemoteFileList error: ${error.message}`);
708
- return new Set();
709
- }
710
- }
711
-
712
- /**
713
- * Validate that an API key is available
714
- * @param {Object} argv - Command line arguments
715
- * @returns {string} - The API key
716
- * @throws {Error} - If no API key is available
717
- */
718
- function validateAuth(argv) {
719
- const apiKey = configManager.getApiKeyWithPriority(argv);
720
- if (!apiKey) {
721
- throw new Error(
722
- 'API key required. Set ROCK_API_KEY environment variable ' +
723
- 'or use --api-key flag'
724
- );
725
- }
726
- return apiKey;
727
- }
728
-
729
- // Export getRemoteFileList for testing
730
- module.exports.getRemoteFileList = getRemoteFileList;
731
-
732
- // Export readSandboxConfig for testing
733
- module.exports.readSandboxConfig = readSandboxConfig;
734
-
735
- // Export validateAuth for testing
736
- module.exports.validateAuth = validateAuth;
737
-
738
- /**
739
- * Ask user for confirmation (interactive mode)
740
- */
741
- async function askUser(question) {
742
- const readline = require('readline');
743
- const rl = readline.createInterface({
744
- input: process.stdin,
745
- output: process.stdout
746
- });
747
-
748
- return new Promise((resolve) => {
749
- rl.question(question, (answer) => {
750
- rl.close();
751
- resolve(answer.toLowerCase().trim());
752
- });
753
- });
754
- }
755
-
756
- /**
757
- * Print upload summary
758
- */
759
- function printUploadSummary(total, results) {
760
- console.log('\nUpload Summary:');
761
- console.log(` Total files: ${total}`);
762
- console.log(` Uploaded: ${results.uploaded}`);
763
- console.log(` Skipped: ${results.skipped} (already exists)`);
764
- console.log(` Failed: ${results.failed}`);
765
-
766
- if (results.failedFiles.length > 0) {
767
- console.log('\nFailed files:');
768
- results.failedFiles.forEach(f => {
769
- console.log(` - ${f.path}: ${f.error}`);
770
- });
771
- }
772
- }
773
-
774
- /**
775
- * Handle directory upload
776
- */
777
- async function handleDirectoryUpload(argv, client) {
778
- const localDir = path.resolve(argv.dir);
779
- const targetPath = argv.targetPath.replace(/\/+$/, ''); // Normalize: remove trailing slashes
780
- const recursive = argv.recursive || false;
781
- const includeHidden = argv.all || false;
782
- const noclobber = argv.noclobber || false;
783
- const clobber = argv.clobber || false;
784
- const interactive = argv.interactive || false;
785
-
786
- // Validate local directory
787
- if (!fs.existsSync(localDir)) {
788
- throw new Error(`Directory not found: ${localDir}`);
789
- }
790
-
791
- const stat = fs.statSync(localDir);
792
- if (!stat.isDirectory()) {
793
- throw new Error(`Not a directory: ${localDir}`);
794
- }
795
-
796
- // Collect files
797
- console.log(`Scanning directory: ${localDir}`);
798
- const files = collectFiles(localDir, { recursive, includeHidden });
799
-
800
- if (files.length === 0) {
801
- console.log('No files to upload');
802
- return;
803
- }
804
-
805
- console.log(`Found ${files.length} file(s) to upload`);
806
- console.log(`Uploading to: ${targetPath}\n`);
807
-
808
- // Get remote file list for conflict detection
809
- const existingFiles = await getRemoteFileList(client, targetPath, recursive);
810
- logger.debug(`Remote files found: ${existingFiles.size}`);
811
-
812
- // Determine conflict strategy
813
- // -n (noclobber) takes precedence: skip existing files
814
- // -c (clobber) takes second precedence: overwrite existing files
815
- // -i (interactive): ask before overwriting
816
- // default: skip existing files
817
- const conflictStrategy = noclobber ? 'skip' : (clobber ? 'overwrite' : (interactive ? 'ask' : 'skip'));
818
-
819
- // Upload files
820
- const results = { uploaded: 0, skipped: 0, failed: 0, failedFiles: [] };
821
- const total = files.length;
822
-
823
- for (let i = 0; i < files.length; i++) {
824
- const file = files[i];
825
- // Convert to POSIX path for remote
826
- const posixRelativePath = file.relativePath.split(path.sep).join('/');
827
- const remoteFilePath = path.posix.join(targetPath, posixRelativePath);
828
- const remoteDir = path.posix.dirname(remoteFilePath);
829
-
830
- // Conflict detection
831
- if (existingFiles.has(remoteFilePath)) {
832
- if (conflictStrategy === 'skip') {
833
- console.log(`[${i + 1}/${total}] Skipped (exists): ${posixRelativePath}`);
834
- results.skipped++;
835
- continue;
836
- } else if (conflictStrategy === 'ask') {
837
- const answer = await askUser(`Overwrite ${remoteFilePath}? [y/n]: `);
838
- if (answer !== 'y') {
839
- console.log(`[${i + 1}/${total}] Skipped: ${posixRelativePath}`);
840
- results.skipped++;
841
- continue;
842
- }
843
- }
844
- // 'overwrite': continue to upload
845
- } else {
846
- logger.debug(`File not in remote list: ${remoteFilePath}, strategy: ${conflictStrategy}`);
847
- }
848
-
849
- console.log(`[${i + 1}/${total}] Uploading: ${posixRelativePath}`);
850
-
851
- try {
852
- // Create remote parent directory if needed (for recursive uploads)
853
- if (recursive && remoteDir !== targetPath) {
854
- await client.execute(['mkdir', '-p', remoteDir]);
855
- }
856
-
857
- // Upload file
858
- const result = await client.uploadFile(file.localPath, remoteFilePath);
859
- if (result.success) {
860
- results.uploaded++;
861
- } else {
862
- results.failed++;
863
- results.failedFiles.push({ path: posixRelativePath, error: result.message });
864
- console.log(`[${i + 1}/${total}] Failed: ${posixRelativePath} - ${result.message}`);
865
- }
866
- } catch (error) {
867
- results.failed++;
868
- results.failedFiles.push({ path: posixRelativePath, error: error.message });
869
- console.log(`[${i + 1}/${total}] Failed: ${posixRelativePath} - ${error.message}`);
870
- }
871
- }
872
-
873
- // Print summary
874
- printUploadSummary(total, results);
875
-
876
- // Exit with error if all files failed
877
- if (results.failed === total) {
878
- gracefulExit(1);
879
- }
880
- }
881
-
882
- /**
883
- * Handle download action
884
- */
885
- async function handleDownload(argv) {
886
- if (!argv.file) {
887
- throw new Error('file is required for download action');
888
- }
889
-
890
- if (!argv.sandboxId) {
891
- throw new Error('sandbox-id is required for download action');
892
- }
893
-
894
- const sandboxConfig = readSandboxConfig(argv);
895
- const baseUrl = argv.baseUrl || sandboxConfig.base_url;
896
- const apiKey = argv.apiKey || sandboxConfig.api_key;
897
- const cluster = argv.cluster || sandboxConfig.cluster;
898
- const userId = argv.userId || sandboxConfig.user_id;
899
- const experimentId = argv.experimentId || sandboxConfig.experiment_id;
900
-
901
- if (!baseUrl) {
902
- throw new Error('base-url is required for download action (use --base-url or run "rock-cli login" first)');
903
- }
904
-
905
- const config = new SandboxConfig({
906
- baseUrl: baseUrl,
907
- xrlAuthorization: apiKey,
908
- cluster: cluster,
909
- userId: userId,
910
- experimentId: experimentId,
911
- extraHeaders: argv.extraHeader || {},
912
- });
913
-
914
- const client = new SandboxClient(config, { requireImage: false });
915
- client._sandboxId = argv.sandboxId;
916
-
917
- console.log(`Downloading ${argv.file}...`);
918
- const result = await client.downloadFile(argv.file);
919
-
920
- console.log('✅ Download successful!');
921
- console.log(` File: ${argv.file}`);
922
- console.log(` Content: ${JSON.stringify(result, null, 2)}`);
923
- }
924
-
925
- /**
926
- * Handle write-file action
927
- */
928
- async function handleWriteFile(argv) {
929
- if (!argv.content) {
930
- throw new Error('content is required for write-file action');
931
- }
932
-
933
- if (!argv.targetPath) {
934
- throw new Error('target-path is required for write-file action');
935
- }
936
-
937
- if (!argv.sandboxId) {
938
- throw new Error('sandbox-id is required for write-file action');
939
- }
940
-
941
- const sandboxConfig = readSandboxConfig(argv);
942
- const baseUrl = argv.baseUrl || sandboxConfig.base_url;
943
- const apiKey = argv.apiKey || sandboxConfig.api_key;
944
- const cluster = argv.cluster || sandboxConfig.cluster;
945
- const userId = argv.userId || sandboxConfig.user_id;
946
- const experimentId = argv.experimentId || sandboxConfig.experiment_id;
947
-
948
- if (!baseUrl) {
949
- throw new Error('base-url is required for write-file action (use --base-url or run "rock-cli login" first)');
950
- }
951
-
952
- const config = new SandboxConfig({
953
- baseUrl: baseUrl,
954
- xrlAuthorization: apiKey,
955
- cluster: cluster,
956
- userId: userId,
957
- experimentId: experimentId,
958
- extraHeaders: argv.extraHeader || {},
959
- });
960
-
961
- const client = new SandboxClient(config, { requireImage: false });
962
- client._sandboxId = argv.sandboxId;
963
-
964
- console.log(`Writing content to ${argv.targetPath}...`);
965
- const result = await client.writeFile(argv.content, argv.targetPath);
966
-
967
- if (result.success) {
968
- console.log('✅ Write successful!');
969
- console.log(` ${result.message}`);
970
- } else {
971
- console.error('❌ Write failed!');
972
- console.error(` ${result.message}`);
973
- gracefulExit(1);
974
- }
975
- }
976
-
977
- /**
978
- * Handle read-file action
979
- */
980
- async function handleReadFile(argv) {
981
- if (!argv.file) {
982
- throw new Error('file is required for read-file action');
983
- }
984
-
985
- if (!argv.sandboxId) {
986
- throw new Error('sandbox-id is required for read-file action');
987
- }
988
-
989
- const sandboxConfig = readSandboxConfig(argv);
990
- const baseUrl = argv.baseUrl || sandboxConfig.base_url;
991
- const apiKey = argv.apiKey || sandboxConfig.api_key;
992
- const cluster = argv.cluster || sandboxConfig.cluster;
993
- const userId = argv.userId || sandboxConfig.user_id;
994
- const experimentId = argv.experimentId || sandboxConfig.experiment_id;
995
-
996
- if (!baseUrl) {
997
- throw new Error('base-url is required for read-file action (use --base-url or run "rock-cli login" first)');
998
- }
999
-
1000
- const config = new SandboxConfig({
1001
- baseUrl: baseUrl,
1002
- xrlAuthorization: apiKey,
1003
- cluster: cluster,
1004
- userId: userId,
1005
- experimentId: experimentId,
1006
- extraHeaders: argv.extraHeader || {},
1007
- });
1008
-
1009
- const client = new SandboxClient(config, { requireImage: false });
1010
- client._sandboxId = argv.sandboxId;
1011
-
1012
- let startLine = argv.startLine || 1;
1013
- let endLine = argv.endLine || 1000;
1014
-
1015
- console.log(`Reading ${argv.file} (lines ${startLine}-${endLine})...`);
1016
- const result = await client.readFile(argv.file, startLine, endLine);
1017
-
1018
- console.log('✅ Read successful!');
1019
- console.log('--- Content ---');
1020
- console.log(result.content);
1021
- console.log('--- End ---');
1022
- }
1023
-
1024
- /**
1025
- * Handle log view action (shorthand: sandbox --id xxx log)
1026
- */
1027
- async function handleLogView(argv) {
1028
- const sandboxId = argv.id || argv.sandboxId;
1029
- if (!sandboxId) {
1030
- console.error('Error: sandbox-id is required');
1031
- process.exit(1);
1032
- }
1033
- const { spawn } = require('child_process');
1034
- spawn('node', [path.join(__dirname, '../all.js'), 'log', 'search', '--sandbox-id', sandboxId], {
1035
- stdio: 'inherit',
1036
- });
1037
- }
1038
-
1039
- /**
1040
- * Wait for sandbox to be alive
1041
- * @param {SandboxClient} client - Sandbox client instance
1042
- * @param {string} sandboxId - Sandbox ID to check
1043
- * @param {number} timeout - Timeout in seconds (default: 120)
1044
- */
1045
- async function waitForSandboxAlive(client, sandboxId, timeout = 120) {
1046
- const startTime = Date.now();
1047
- const timeoutMs = timeout * 1000;
1048
-
1049
- while (Date.now() - startTime < timeoutMs) {
1050
- try {
1051
- const status = await client.getStatus();
1052
- logger.debug(`Sandbox status: ${JSON.stringify(status)}`);
1053
-
1054
- if (status.isAlive) {
1055
- return;
1056
- }
1057
-
1058
- // Check for failures
1059
- if (status.status) {
1060
- for (const [stage, details] of Object.entries(status.status)) {
1061
- if (details.status === 'failed' || details.status === 'timeout') {
1062
- // Ignore ray_schedule failure during wait for alive
1063
- if (stage === 'ray_schedule') {
1064
- logger.debug(`Ignoring ray_schedule failure during wait for alive: ${details.message || 'Unknown error'}`);
1065
- continue;
1066
- }
1067
- throw new Error(`Sandbox failed at ${stage}: ${details.message || 'Unknown error'}`);
1068
- }
1069
- }
1070
- }
1071
-
1072
- await new Promise(resolve => setTimeout(resolve, 3000));
1073
- } catch (error) {
1074
- logger.warn(`Failed to check sandbox status: ${error.message}`);
1075
- throw error;
1076
- }
1077
- }
1078
-
1079
- throw new Error(`Sandbox did not become alive within ${timeout}s`);
1080
- }
1081
-
1082
- /**
1083
- * Handle replay action
1084
- */
1085
- async function handleReplay(argv) {
1086
- const sandboxConfig = readSandboxConfig(argv);
1087
- const baseUrl = argv.baseUrl || sandboxConfig.base_url;
1088
- const apiKey = argv.apiKey || sandboxConfig.api_key;
1089
- const cluster = argv.cluster || sandboxConfig.cluster;
1090
- const userId = argv.userId || sandboxConfig.user_id;
1091
- const experimentId = argv.experimentId || sandboxConfig.experiment_id;
1092
-
1093
- if (!baseUrl) {
1094
- throw new Error('base-url is required for relay action (use --base-url or run "rock-cli login" first)');
1095
- }
1096
-
1097
- const config = new SandboxConfig({
1098
- baseUrl: baseUrl,
1099
- xrlAuthorization: apiKey,
1100
- cluster: cluster,
1101
- userId: userId,
1102
- experimentId: experimentId,
1103
- extraHeaders: argv.extraHeader || {},
1104
- });
1105
-
1106
- const client = new SandboxClient(config, { requireImage: false });
1107
-
1108
- // Initialize log file
1109
- const fs = require('fs');
1110
- const path = require('path');
1111
- const logFilePath = argv.logFile || 'replay.log';
1112
- const logStream = fs.createWriteStream(logFilePath, { flags: 'a' });
1113
-
1114
- const logToFile = (message) => {
1115
- const timestamp = new Date().toISOString();
1116
- logStream.write(`[${timestamp}] ${message}\n`);
1117
- };
1118
-
1119
- logToFile('=== Replay Session Started ===');
1120
- logToFile(`Base URL: ${baseUrl}`);
1121
- logToFile(`Cluster: ${argv.cluster || 'default'}`);
1122
- logToFile(`Interval: ${argv.interval}s`);
1123
- logToFile(`Stop requests: ${argv.stop ? 'enabled' : 'disabled'}`);
1124
-
1125
- // Read JSON from stdin
1126
- let inputData = '';
1127
- return new Promise((resolve, reject) => {
1128
- process.stdin.setEncoding('utf8');
1129
- process.stdin.on('data', (chunk) => {
1130
- inputData += chunk;
1131
- });
1132
- process.stdin.on('end', async () => {
1133
- try {
1134
- let data;
1135
- try {
1136
- data = JSON.parse(inputData);
1137
- } catch (parseError) {
1138
- throw new Error(`Failed to parse JSON from stdin: ${parseError.message}`);
1139
- }
1140
-
1141
- if (!data.requests || !Array.isArray(data.requests)) {
1142
- throw new Error('Invalid input format: "requests" array is required');
1143
- }
1144
-
1145
- logToFile(`Loaded ${data.requests.length} requests to replay`);
1146
- console.log(`📋 Loaded ${data.requests.length} requests to replay`);
1147
- console.log(`📝 Logging to: ${logFilePath}`);
1148
- console.log('⏳ Starting request replay...\n');
1149
-
1150
- let successCount = 0;
1151
- let failCount = 0;
1152
- let currentSandboxId = null;
1153
-
1154
- for (let i = 0; i < data.requests.length; i++) {
1155
- const request = data.requests[i];
1156
- const requestNum = i + 1;
1157
-
1158
- // Replace sandbox_id in request if we have a current one
1159
- if (currentSandboxId) {
1160
- // Replace sandbox_id in request body
1161
- if (request.requestBody && request.requestBody.sandbox_id) {
1162
- request.requestBody.sandbox_id = currentSandboxId;
1163
- logger.debug(`Replaced sandbox_id in body with: ${currentSandboxId}`);
1164
- }
1165
- // Replace sandbox_id in URI query parameter
1166
- if (request.uri.includes('sandbox_id=')) {
1167
- request.uri = request.uri.replace(/sandbox_id=[^&]+/, `sandbox_id=${currentSandboxId}`);
1168
- logger.debug(`Replaced sandbox_id in URI with: ${currentSandboxId}`);
1169
- }
1170
- }
1171
-
1172
- console.log(`[${requestNum}/${data.requests.length}] ${request.method} ${request.uri}`);
1173
- logToFile(`[${requestNum}/${data.requests.length}] ${request.method} ${request.uri}`);
1174
- logToFile(` Request Body: ${JSON.stringify(request.requestBody || null)}`);
1175
- logToFile(` Headers: ${JSON.stringify(request.headers || null)}`);
1176
-
1177
- // Skip stop request unless --stop flag is provided
1178
- if (request.uri.includes('/stop')) {
1179
- if (!argv.stop) {
1180
- console.log(` ⏭️ Skipped (use --stop to execute stop requests)`);
1181
- logToFile(` ⏭️ Skipped (stop requests disabled)`);
1182
- continue;
1183
- }
1184
- }
1185
-
1186
- const requestStartTime = Date.now();
1187
- try {
1188
- let response;
1189
- // Handle upload request specially
1190
- if (request.uri.includes('upload') && request.requestBody && request.requestBody.file) {
1191
- const fileData = request.requestBody.file;
1192
- const filename = fileData.filename;
1193
- const content = fileData.content;
1194
- const targetPath = request.requestBody.target_path || `/tmp/${filename}`;
1195
-
1196
- logger.debug(`Handling upload request: ${filename} -> ${targetPath}`);
1197
-
1198
- // Write content to a temporary file
1199
- const fs = require('fs');
1200
- const os = require('os');
1201
- const path = require('path');
1202
- const tempDir = os.tmpdir();
1203
- const tempFilePath = path.join(tempDir, filename);
1204
-
1205
- // Decode file content (base64 or escaped binary)
1206
- let fileBuffer;
1207
- if (fileData.isBase64) {
1208
- fileBuffer = Buffer.from(content, 'base64');
1209
- } else {
1210
- // Fallback: parse escaped binary string for backward compatibility
1211
- const parseEscapedBinary = (str) => {
1212
- const buffer = [];
1213
- let i = 0;
1214
- while (i < str.length) {
1215
- if (str[i] === '\\' && i + 1 < str.length) {
1216
- if (str[i + 1] === 'x' && i + 3 < str.length) {
1217
- const hexCode = str.substr(i + 2, 2);
1218
- const byteValue = parseInt(hexCode, 16);
1219
- buffer.push(byteValue);
1220
- i += 4;
1221
- } else {
1222
- const char = str[i + 1];
1223
- switch (char) {
1224
- case 'n': buffer.push(0x0A); i += 2; break;
1225
- case 'r': buffer.push(0x0D); i += 2; break;
1226
- case 't': buffer.push(0x09); i += 2; break;
1227
- case '\\': buffer.push(0x5C); i += 2; break;
1228
- default: buffer.push(char.charCodeAt(0)); i += 2; break;
1229
- }
1230
- }
1231
- } else {
1232
- buffer.push(str.charCodeAt(i));
1233
- i++;
1234
- }
1235
- }
1236
- return Buffer.from(buffer);
1237
- };
1238
- fileBuffer = parseEscapedBinary(content);
1239
- }
1240
-
1241
- fs.writeFileSync(tempFilePath, fileBuffer);
1242
- logger.debug(`Created temporary file: ${tempFilePath} (${fileBuffer.length} bytes)`);
1243
- logger.debug(`First 16 bytes: ${fileBuffer.slice(0, 16).toString('hex')}`);
1244
-
1245
- // Set sandbox_id for client
1246
- client._sandboxId = currentSandboxId;
1247
-
1248
- // Use SDK's uploadFile method
1249
- const result = await client.uploadFile(tempFilePath, targetPath);
1250
-
1251
- // Clean up temp file
1252
- fs.unlinkSync(tempFilePath);
1253
-
1254
- if (result.success) {
1255
- successCount++;
1256
- const duration = Date.now() - requestStartTime;
1257
- console.log(` ✅ Success`);
1258
- logToFile(` ✅ Success - Duration: ${duration}ms - Message: ${result.message}`);
1259
- logToFile(` Response Body: ${JSON.stringify(result)}`);
1260
- logToFile(` Response Headers: ${JSON.stringify(result.headers || {})}`);
1261
- logger.debug(` Upload result: ${result.message}`);
1262
- } else {
1263
- throw new Error(result.message);
1264
- }
1265
- } else {
1266
- // Use generic makeRequest for other requests
1267
- response = await client.makeRequest(request);
1268
- successCount++;
1269
- const duration = Date.now() - requestStartTime;
1270
- console.log(` ✅ Success - Status: ${response.status || 200}`);
1271
- logToFile(` ✅ Success - Status: ${response.status || 200} - Duration: ${duration}ms`);
1272
- logToFile(` Response Body: ${JSON.stringify(response.data)}`);
1273
- logToFile(` Response Headers: ${JSON.stringify(response.headers || {})}`);
1274
- logger.debug(` Response: ${JSON.stringify(response)}`);
1275
- }
1276
-
1277
- // Check if this is a start_async request and extract sandbox_id
1278
- if (request.uri.includes('start_async') && response && response.data && response.data.result && response.data.result.sandbox_id) {
1279
- currentSandboxId = response.data.result.sandbox_id;
1280
- console.log(` 📝 Extracted sandbox_id: ${currentSandboxId}`);
1281
- logToFile(` 📝 Extracted sandbox_id: ${currentSandboxId}`);
1282
- }
1283
- } catch (error) {
1284
- failCount++;
1285
- const duration = Date.now() - requestStartTime;
1286
- console.log(` ❌ Failed - ${error.message}`);
1287
- logToFile(` ❌ Failed - Duration: ${duration}ms - Error: ${error.message}`);
1288
-
1289
- // For axios errors, log the backend response data instead of stack trace
1290
- if (error.response && error.response.data) {
1291
- logToFile(` Response Data: ${JSON.stringify(error.response.data)}`);
1292
- }
1293
-
1294
- logger.error(`Request ${requestNum} failed: ${error.message}`);
1295
- }
1296
-
1297
- // Wait between requests (except for the last one)
1298
- if (i < data.requests.length - 1) {
1299
- if (request.uri.includes('start_async')) {
1300
- // Wait for sandbox to be alive after start_async
1301
- console.log(' ⏳ Waiting for sandbox to be alive...');
1302
- logToFile(` ⏳ Waiting for sandbox to be alive...`);
1303
- client._sandboxId = currentSandboxId;
1304
- await waitForSandboxAlive(client, currentSandboxId);
1305
- console.log(' ✅ Sandbox is alive!\n');
1306
- logToFile(` ✅ Sandbox is alive!`);
1307
- } else if (argv.interval > 0) {
1308
- // Wait for specified interval
1309
- console.log(` ⏸️ Waiting ${argv.interval} seconds...\n`);
1310
- logToFile(` ⏸️ Waiting ${argv.interval} seconds...`);
1311
- await new Promise(resolve => setTimeout(resolve, argv.interval * 1000));
1312
- } else {
1313
- console.log();
1314
- }
1315
- } else {
1316
- console.log();
1317
- }
1318
- }
1319
-
1320
- console.log('📊 Replay Summary:');
1321
- console.log(` - Total requests: ${data.requests.length}`);
1322
- console.log(` - ✅ Successful: ${successCount}`);
1323
- console.log(` - ❌ Failed: ${failCount}`);
1324
-
1325
- logToFile('=== Replay Summary ===');
1326
- logToFile(`Total requests: ${data.requests.length}`);
1327
- logToFile(`Successful: ${successCount}`);
1328
- logToFile(`Failed: ${failCount}`);
1329
- logToFile('=== Replay Session Ended ===');
1330
-
1331
- logStream.end();
1332
- console.log(`📝 Log saved to: ${logFilePath}`);
1333
-
1334
- resolve();
1335
- } catch (error) {
1336
- logToFile(`ERROR: ${error.message}`);
1337
- logStream.end();
1338
- reject(error);
1339
- }
1340
- });
1341
- process.stdin.on('error', (error) => {
1342
- logToFile(`ERROR: Error reading from stdin: ${error.message}`);
1343
- logStream.end();
1344
- reject(new Error(`Error reading from stdin: ${error.message}`));
1345
- });
1346
- });
1347
- }
1348
-
1349
- /**
1350
- * Handle legacy build, push, run actions (backward compatibility)
1351
- */
1352
- async function handleLegacyActions(argv) {
1353
- const { exec } = require('child_process');
1354
- const util = require('util');
1355
- const execAsync = util.promisify(exec);
1356
-
1357
- switch (argv.action) {
1358
- case 'build':
1359
- if (!argv.tag) {
1360
- throw new Error('tag is required for build action');
1361
- }
1362
- let dockerCommand = `docker build -t ${argv.tag} -f ${argv.file || 'Dockerfile'} .`;
1363
- if (argv.cpus) dockerCommand += ` --cpus=${argv.cpus}`;
1364
- if (argv.memory) dockerCommand += ` --memory=${argv.memory}`;
1365
-
1366
- console.log(`Building sandbox: ${dockerCommand}`);
1367
- const { stdout, stderr } = await execAsync(dockerCommand);
1368
- console.log(stdout);
1369
- if (stderr) console.error(stderr);
1370
- console.log('✅ Build successful!');
1371
- break;
1372
-
1373
- case 'push':
1374
- if (!argv.image) {
1375
- throw new Error('image is required for push action');
1376
- }
1377
- const registry = argv.registry || 'docker.io';
1378
- const tagCommand = `docker tag ${argv.image} ${registry}/${argv.image}`;
1379
- await execAsync(tagCommand);
1380
-
1381
- const pushCommand = `docker push ${registry}/${argv.image}`;
1382
- console.log(`Pushing: ${pushCommand}`);
1383
- const pushResult = await execAsync(pushCommand);
1384
- console.log(pushResult.stdout);
1385
- console.log('✅ Push successful!');
1386
- break;
1387
-
1388
- case 'run':
1389
- console.log('Note: Use "rock-cli sandbox start" + "rock-cli sandbox execute" for run functionality');
1390
- console.log('The legacy "run" action is deprecated and will be removed in a future version');
1391
- break;
1392
- }
1393
- }