rl-rockcli 0.0.8 → 0.0.9
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.
- package/commands/log/core/constants.js +237 -0
- package/commands/log/core/display.js +370 -0
- package/commands/log/core/search.js +330 -0
- package/commands/log/core/tail.js +216 -0
- package/commands/log/core/utils.js +424 -0
- package/commands/log.js +298 -0
- package/commands/sandbox/core/log-bridge.js +119 -0
- package/commands/sandbox/core/replay/analyzer.js +311 -0
- package/commands/sandbox/core/replay/batch-orchestrator.js +536 -0
- package/commands/sandbox/core/replay/batch-task.js +369 -0
- package/commands/sandbox/core/replay/concurrent-display.js +70 -0
- package/commands/sandbox/core/replay/concurrent-orchestrator.js +170 -0
- package/commands/sandbox/core/replay/data-source.js +86 -0
- package/commands/sandbox/core/replay/display.js +231 -0
- package/commands/sandbox/core/replay/executor.js +634 -0
- package/commands/sandbox/core/replay/history-fetcher.js +124 -0
- package/commands/sandbox/core/replay/index.js +338 -0
- package/commands/sandbox/core/replay/loghouse-data-source.js +177 -0
- package/commands/sandbox/core/replay/pid-mapping.js +26 -0
- package/commands/sandbox/core/replay/request.js +109 -0
- package/commands/sandbox/core/replay/worker.js +166 -0
- package/commands/sandbox/core/session.js +346 -0
- package/commands/sandbox/log-bridge.js +2 -0
- package/commands/sandbox/ray.js +2 -0
- package/commands/sandbox/replay/analyzer.js +311 -0
- package/commands/sandbox/replay/batch-orchestrator.js +536 -0
- package/commands/sandbox/replay/batch-task.js +369 -0
- package/commands/sandbox/replay/concurrent-display.js +70 -0
- package/commands/sandbox/replay/concurrent-orchestrator.js +170 -0
- package/commands/sandbox/replay/display.js +231 -0
- package/commands/sandbox/replay/executor.js +634 -0
- package/commands/sandbox/replay/history-fetcher.js +118 -0
- package/commands/sandbox/replay/index.js +338 -0
- package/commands/sandbox/replay/pid-mapping.js +26 -0
- package/commands/sandbox/replay/request.js +109 -0
- package/commands/sandbox/replay/worker.js +166 -0
- package/commands/sandbox/replay.js +2 -0
- package/commands/sandbox/session.js +2 -0
- package/commands/sandbox-original.js +1393 -0
- package/commands/sandbox.js +499 -0
- package/help/help.json +1071 -0
- package/help/middleware.js +71 -0
- package/help/renderer.js +800 -0
- package/index.js +5 -15
- package/lib/plugin-context.js +40 -0
- package/package.json +2 -2
- package/sdks/sandbox/core/client.js +845 -0
- package/sdks/sandbox/core/config.js +70 -0
- package/sdks/sandbox/core/types.js +74 -0
- package/sdks/sandbox/httpLogger.js +251 -0
- package/sdks/sandbox/index.js +9 -0
- package/utils/asciiArt.js +138 -0
- package/utils/bun-compat.js +59 -0
- package/utils/ciPipelines.js +138 -0
- package/utils/cli.js +17 -0
- package/utils/command-router.js +79 -0
- package/utils/configManager.js +503 -0
- package/utils/dependency-resolver.js +135 -0
- package/utils/eagleeye_traceid.js +151 -0
- package/utils/envDetector.js +78 -0
- package/utils/execution_logger.js +415 -0
- package/utils/featureManager.js +68 -0
- package/utils/firstTimeTip.js +44 -0
- package/utils/hook-manager.js +125 -0
- package/utils/http-logger.js +264 -0
- package/utils/i18n.js +139 -0
- package/utils/image-progress.js +159 -0
- package/utils/logger.js +154 -0
- package/utils/plugin-loader.js +124 -0
- package/utils/plugin-manager.js +348 -0
- package/utils/ray_cli_wrapper.js +746 -0
- package/utils/sandbox-client.js +419 -0
- package/utils/terminal.js +32 -0
- package/utils/tips.js +106 -0
|
@@ -0,0 +1,1393 @@
|
|
|
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 || process.env.ROCKCLI_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
|
+
}
|