rl-rockcli 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ROCK CLI Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/bin/rockcli.js ADDED
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * rockcli 入口脚本
5
+ * 开源版 CLI Shell,负责加载 core + 指定插件集合
6
+ */
7
+
8
+ const yargsFactory = require('yargs/yargs');
9
+ const { hideBin } = require('yargs/helpers');
10
+
11
+ // 加载 Core 命令
12
+ const logCommand = require('../src/core/commands/log');
13
+ const sandboxCommand = require('../src/core/commands/sandbox');
14
+ const attachCommand = require('../src/core/commands/attach');
15
+
16
+ function main(argv = process.argv) {
17
+ const yargs = yargsFactory(hideBin(argv));
18
+
19
+ yargs
20
+ .scriptName('rockcli')
21
+ .usage('$0 <command> [options]')
22
+ // 注册命令
23
+ .command(logCommand)
24
+ .command(sandboxCommand)
25
+ .command(attachCommand)
26
+ .demandCommand(1, 'Please provide a command, e.g.: log search, sandbox start')
27
+ .strict()
28
+ .help()
29
+ .alias('h', 'help')
30
+ .wrap(Math.min(120, yargs.terminalWidth ? yargs.terminalWidth() : 100))
31
+ .parse();
32
+ }
33
+
34
+ if (require.main === module) {
35
+ main();
36
+ }
37
+
38
+ module.exports = {
39
+ main,
40
+ };
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "rl-rockcli",
3
+ "version": "0.0.1",
4
+ "description": "Open source ROCK CLI - Sandbox and Log management tool",
5
+ "main": "src/core/index.js",
6
+ "bin": {
7
+ "rockcli": "bin/rockcli.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node bin/rockcli.js",
11
+ "test": "jest --testPathIgnorePatterns=integration",
12
+ "test:coverage": "jest --coverage --testPathIgnorePatterns=integration",
13
+ "test:integration": "jest --testPathPattern=integration",
14
+ "test:all": "jest",
15
+ "prepublishOnly": "npm test",
16
+ "prepack": "echo 'Preparing package...'",
17
+ "publish:dry": "npm pack --dry-run",
18
+ "publish:check": "bash scripts/prepublish-check.sh",
19
+ "release": "bash scripts/publish.sh",
20
+ "release:alpha": "bash scripts/publish.sh prerelease --preid=alpha",
21
+ "release:beta": "bash scripts/publish.sh prerelease --preid=beta"
22
+ },
23
+ "files": [
24
+ "bin/",
25
+ "src/core/",
26
+ "src/plugins/"
27
+ ],
28
+ "keywords": [
29
+ "cli",
30
+ "rock",
31
+ "sandbox",
32
+ "logs",
33
+ "developer-tools"
34
+ ],
35
+ "author": "",
36
+ "license": "MIT",
37
+ "dependencies": {
38
+ "@opentui/core": "^0.1.77",
39
+ "@opentui/solid": "^0.1.77",
40
+ "axios": "^1.6.0",
41
+ "clipboardy": "^5.2.0",
42
+ "fs-extra": "^11.1.1",
43
+ "ink": "npm:@jrichman/ink@6.4.8",
44
+ "logfmt": "^1.4.0",
45
+ "react": "^19.0.0",
46
+ "solid-js": "^1.9.0",
47
+ "winston": "^3.19.0",
48
+ "yargs": "^17.7.2"
49
+ },
50
+ "devDependencies": {
51
+ "@babel/core": "^7.29.0",
52
+ "@babel/preset-react": "^7.28.5",
53
+ "jest": "^29.7.0"
54
+ },
55
+ "engines": {
56
+ "node": ">=14.0.0"
57
+ },
58
+ "repository": {
59
+ "type": "git",
60
+ "url": "https://github.com/rock-cli/rockcli.git"
61
+ }
62
+ }
@@ -0,0 +1,242 @@
1
+ /**
2
+ * attach 命令实现
3
+ * 开源版 attach 命令 - 交互式 REPL
4
+ */
5
+
6
+ const { SandboxClient, SandboxConfig } = require('../../sdks/sandbox');
7
+ const logger = require('../../utils/logger');
8
+ const fs = require('fs-extra');
9
+ const path = require('path');
10
+ const readline = require('readline');
11
+
12
+ // 配置路径
13
+ const CONFIG_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.rock');
14
+ const HISTORY_DIR = path.join(CONFIG_DIR, 'history');
15
+
16
+ /**
17
+ * 内置命令列表
18
+ */
19
+ const BUILTIN_COMMANDS = [
20
+ { name: 'log', description: 'View sandbox logs' },
21
+ { name: 'upload', description: 'Upload file to sandbox' },
22
+ { name: 'download', description: 'Download file from sandbox' },
23
+ { name: 'status', description: 'Show sandbox status' },
24
+ { name: 'stop', description: 'Stop the sandbox' },
25
+ { name: 'clear', description: 'Clear terminal screen' },
26
+ { name: 'help', description: 'Show help message' },
27
+ { name: 'exit', description: 'Exit the REPL (session kept alive)' },
28
+ { name: 'close', description: 'Close session and exit' },
29
+ ];
30
+
31
+ /**
32
+ * attach 命令模块
33
+ */
34
+ module.exports = {
35
+ command: 'attach <sandboxId>',
36
+ describe: 'Attach to a sandbox with interactive REPL',
37
+ builder: (yargs) => {
38
+ return yargs
39
+ .positional('sandboxId', {
40
+ describe: 'Sandbox ID to attach to',
41
+ type: 'string',
42
+ })
43
+ .option('api-key', {
44
+ describe: 'API key for authentication',
45
+ type: 'string',
46
+ })
47
+ .option('base-url', {
48
+ describe: 'Base URL for sandbox service',
49
+ type: 'string',
50
+ default: process.env.ROCK_BASE_URL || 'http://localhost:8080',
51
+ })
52
+ .option('ui', {
53
+ describe: 'UI mode (basic/opentui)',
54
+ type: 'string',
55
+ default: 'basic',
56
+ });
57
+ },
58
+ handler: async (argv) => {
59
+ const sandboxId = argv.sandboxId;
60
+ const apiKey = argv.apiKey || process.env.ROCK_API_KEY;
61
+ const baseUrl = argv.baseUrl;
62
+
63
+ if (!apiKey) {
64
+ console.error('Error: API key required. Set ROCK_API_KEY env var or use --api-key.');
65
+ process.exit(1);
66
+ }
67
+
68
+ // 创建客户端 - attach 不需要 image 参数
69
+ const config = new SandboxConfig({
70
+ baseUrl: baseUrl,
71
+ xrlAuthorization: apiKey,
72
+ });
73
+
74
+ const client = new SandboxClient(config, { requireImage: false });
75
+ client._sandboxId = sandboxId;
76
+
77
+ // 检查 sandbox 状态
78
+ try {
79
+ const status = await client.getStatus();
80
+ if (!status.is_alive) {
81
+ console.error(`Sandbox ${sandboxId} is not alive`);
82
+ process.exit(1);
83
+ }
84
+ console.log(`Connected to sandbox ${sandboxId}`);
85
+ console.log(`Image: ${status.image || 'N/A'}`);
86
+ console.log(`Status: alive`);
87
+ console.log('');
88
+ } catch (error) {
89
+ console.error(`Failed to connect to sandbox: ${error.message}`);
90
+ process.exit(1);
91
+ }
92
+
93
+ // 启动 REPL
94
+ await startREPL(client, sandboxId);
95
+ },
96
+ };
97
+
98
+ /**
99
+ * 启动 REPL
100
+ */
101
+ async function startREPL(client, sandboxId) {
102
+ console.log('Type /help for available commands, or enter shell commands to execute.');
103
+ console.log('');
104
+
105
+ // 创建 session
106
+ try {
107
+ await client.createSession({ session: 'default' });
108
+ } catch (error) {
109
+ logger.debug(`Session creation: ${error.message}`);
110
+ }
111
+
112
+ const rl = readline.createInterface({
113
+ input: process.stdin,
114
+ output: process.stdout,
115
+ prompt: `rockcli:${sandboxId.slice(0, 8)}> `,
116
+ });
117
+
118
+ rl.prompt();
119
+
120
+ rl.on('line', async (input) => {
121
+ const trimmed = input.trim();
122
+
123
+ if (!trimmed) {
124
+ rl.prompt();
125
+ return;
126
+ }
127
+
128
+ // 处理内置命令
129
+ if (trimmed.startsWith('/')) {
130
+ await handleBuiltinCommand(trimmed, client, sandboxId, rl);
131
+ rl.prompt();
132
+ return;
133
+ }
134
+
135
+ // 执行 shell 命令
136
+ try {
137
+ const result = await client.execute(trimmed);
138
+ if (result.stdout) console.log(result.stdout);
139
+ if (result.stderr) console.error(result.stderr);
140
+ } catch (error) {
141
+ console.error(`Error: ${error.message}`);
142
+ }
143
+
144
+ rl.prompt();
145
+ });
146
+
147
+ rl.on('close', () => {
148
+ console.log('Exiting REPL...');
149
+ process.exit(0);
150
+ });
151
+ }
152
+
153
+ /**
154
+ * 处理内置命令
155
+ */
156
+ async function handleBuiltinCommand(cmd, client, sandboxId, rl) {
157
+ const parts = cmd.slice(1).split(/\s+/);
158
+ const command = parts[0].toLowerCase();
159
+ const args = parts.slice(1);
160
+
161
+ switch (command) {
162
+ case 'help':
163
+ console.log('Available commands:');
164
+ BUILTIN_COMMANDS.forEach(c => {
165
+ console.log(` /${c.name.padEnd(10)} - ${c.description}`);
166
+ });
167
+ console.log('');
168
+ console.log('Or enter any shell command to execute in the sandbox.');
169
+ break;
170
+
171
+ case 'status':
172
+ try {
173
+ const status = await client.getStatus();
174
+ console.log(JSON.stringify(status, null, 2));
175
+ } catch (error) {
176
+ console.error(`Error: ${error.message}`);
177
+ }
178
+ break;
179
+
180
+ case 'log':
181
+ console.log('Log viewing not implemented in basic mode');
182
+ break;
183
+
184
+ case 'upload':
185
+ if (args.length < 2) {
186
+ console.log('Usage: /upload <local-path> <remote-path>');
187
+ } else {
188
+ try {
189
+ const result = await client.uploadFile(args[0], args[1]);
190
+ console.log(result.message);
191
+ } catch (error) {
192
+ console.error(`Error: ${error.message}`);
193
+ }
194
+ }
195
+ break;
196
+
197
+ case 'download':
198
+ if (args.length < 1) {
199
+ console.log('Usage: /download <remote-path> [local-path]');
200
+ } else {
201
+ try {
202
+ const result = await client.downloadFile(args[0]);
203
+ console.log(result.content || JSON.stringify(result, null, 2));
204
+ } catch (error) {
205
+ console.error(`Error: ${error.message}`);
206
+ }
207
+ }
208
+ break;
209
+
210
+ case 'stop':
211
+ try {
212
+ await client.stop();
213
+ console.log(`Sandbox ${sandboxId} stopped`);
214
+ rl.close();
215
+ } catch (error) {
216
+ console.error(`Error: ${error.message}`);
217
+ }
218
+ break;
219
+
220
+ case 'clear':
221
+ console.clear();
222
+ break;
223
+
224
+ case 'exit':
225
+ console.log('Exiting REPL (sandbox remains running)');
226
+ rl.close();
227
+ break;
228
+
229
+ case 'close':
230
+ try {
231
+ await client.closeSession('default');
232
+ } catch (error) {
233
+ // 忽略错误
234
+ }
235
+ console.log('Session closed');
236
+ rl.close();
237
+ break;
238
+
239
+ default:
240
+ console.log(`Unknown command: /${command}. Type /help for available commands.`);
241
+ }
242
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * log 命令常量
3
+ * 复用 display 层的常量定义
4
+ */
5
+
6
+ const {
7
+ DISPLAY_FIELDS,
8
+ FIELD_COLOR_MAP,
9
+ DEFAULT_FIELD_COLOR,
10
+ TRUNCATE_FIELDS,
11
+ ANSI_COLORS,
12
+ } = require('../../display/constants');
13
+
14
+ module.exports = {
15
+ DISPLAY_FIELDS,
16
+ FIELD_COLOR_MAP,
17
+ DEFAULT_FIELD_COLOR,
18
+ TRUNCATE_FIELDS,
19
+ ANSI_COLORS,
20
+ };
@@ -0,0 +1,94 @@
1
+ /**
2
+ * log 命令入口
3
+ */
4
+
5
+ const { handleLogSearch } = require('./search');
6
+
7
+ /**
8
+ * log 命令定义(yargs 格式)
9
+ */
10
+ module.exports = {
11
+ command: 'log <action>',
12
+ describe: 'Log operations (currently only supports "search" on local log files)',
13
+ builder: (yargs) => {
14
+ return yargs
15
+ .positional('action', {
16
+ describe: 'Log action to perform',
17
+ type: 'string',
18
+ choices: ['search'],
19
+ })
20
+ .option('keyword', {
21
+ alias: 'k',
22
+ describe: 'Search keyword (space-separated keywords will be ANDed)',
23
+ type: 'string',
24
+ })
25
+ .option('minutes', {
26
+ alias: 'm',
27
+ describe: 'Search logs from last N minutes',
28
+ type: 'number',
29
+ group: 'Time Range:',
30
+ })
31
+ .option('start-time', {
32
+ describe: 'Start time (timestamp, relative time like "15m", "1h", or date string)',
33
+ type: 'string',
34
+ group: 'Time Range:',
35
+ })
36
+ .option('end-time', {
37
+ describe: 'End time (same format as start-time, defaults to now when omitted)',
38
+ type: 'string',
39
+ group: 'Time Range:',
40
+ })
41
+ .option('limit', {
42
+ alias: 'l',
43
+ describe: 'Max number of logs to return',
44
+ type: 'number',
45
+ default: 100,
46
+ })
47
+ .option('offset', {
48
+ alias: 'o',
49
+ describe: 'Number of logs to skip before returning results',
50
+ type: 'number',
51
+ default: 0,
52
+ })
53
+ .option('log-format', {
54
+ describe: 'Output format: logfmt or json',
55
+ type: 'string',
56
+ default: 'logfmt',
57
+ })
58
+ .option('columns', {
59
+ describe: 'Comma-separated list of fields to display',
60
+ type: 'string',
61
+ })
62
+ .option('truncate', {
63
+ describe: 'Truncate long field values (0 to disable)',
64
+ type: 'number',
65
+ default: 2048,
66
+ })
67
+ .option('highlight', {
68
+ describe: 'Enable keyword highlight in values',
69
+ type: 'boolean',
70
+ default: true,
71
+ })
72
+ .option('sandbox-id', {
73
+ describe: 'Filter logs by sandbox_id field',
74
+ type: 'string',
75
+ })
76
+ .option('event', {
77
+ describe: 'Filter logs by event field',
78
+ type: 'string',
79
+ })
80
+ .example([
81
+ ['$0 log search -m 15 -k "error"', 'Search local CLI logs in last 15 minutes with keyword "error"'],
82
+ ]);
83
+ },
84
+ handler: async (argv) => {
85
+ const { action } = argv;
86
+ if (action === 'search') {
87
+ await handleLogSearch(argv);
88
+ return;
89
+ }
90
+ // eslint-disable-next-line no-console
91
+ console.error(`Unsupported log action: ${action}`);
92
+ process.exitCode = 1;
93
+ },
94
+ };
@@ -0,0 +1,106 @@
1
+ /**
2
+ * log search 命令实现
3
+ * 依赖 LogProvider 接口,不依赖具体实现
4
+ */
5
+
6
+ const { parseTime } = require('../../utils/time');
7
+ const { displayLogs } = require('../../display/format');
8
+
9
+ /**
10
+ * log search 命令处理函数
11
+ * @param {Object} argv - 命令行参数
12
+ * @param {Object} provider - LogProvider 实例(注入)
13
+ */
14
+ async function handleLogSearch(argv, provider = null) {
15
+ const {
16
+ keyword,
17
+ minutes,
18
+ startTime,
19
+ endTime,
20
+ limit = 100,
21
+ offset = 0,
22
+ logFormat = 'logfmt',
23
+ columns,
24
+ truncate = 2048,
25
+ highlight = true,
26
+ sandboxId,
27
+ event,
28
+ } = argv;
29
+
30
+ let actualStart;
31
+ let actualEnd;
32
+
33
+ if (minutes) {
34
+ const m = Number(minutes);
35
+ if (!Number.isFinite(m) || m <= 0) {
36
+ throw new Error('--minutes 必须是正数');
37
+ }
38
+ actualEnd = Date.now();
39
+ actualStart = actualEnd - m * 60 * 1000;
40
+ } else {
41
+ actualStart = parseTime(startTime);
42
+ actualEnd = endTime ? parseTime(endTime) : Date.now();
43
+ }
44
+
45
+ if (!actualStart) {
46
+ throw new Error('必须指定 --minutes 或 --start-time');
47
+ }
48
+
49
+ if (actualEnd == null) {
50
+ actualEnd = Date.now();
51
+ }
52
+
53
+ // 归一化为秒对齐
54
+ actualStart = Math.floor(actualStart / 1000) * 1000;
55
+ actualEnd = Math.floor(actualEnd / 1000) * 1000;
56
+
57
+ // 使用注入的 provider 或从默认路径创建
58
+ let client = provider;
59
+ if (!client) {
60
+ // 默认使用 file-client(向后兼容)
61
+ const createFileLogClient = require('../../../plugins/oss-file-log/file-client');
62
+ client = createFileLogClient();
63
+ }
64
+
65
+ await client.connect();
66
+
67
+ // 按接口契约传递所有标准字段(包括 table/orderBy/orderDirection),
68
+ // 由具体实现决定是否使用(开源 file backend 会忽略 table 等字段)
69
+ const logs = await client.query({
70
+ startTime: actualStart,
71
+ endTime: actualEnd,
72
+ filter: keyword ? String(keyword) : null,
73
+ limit,
74
+ offset,
75
+ count: false,
76
+ table: null, // 开源版无 table 概念,传 null
77
+ orderBy: '@timestamp',
78
+ orderDirection: 'ASC',
79
+ sandboxId,
80
+ event,
81
+ });
82
+
83
+ await client.close();
84
+
85
+ let resultLogs = logs || [];
86
+
87
+ if (sandboxId) {
88
+ resultLogs = resultLogs.filter((log) => log && log.sandbox_id === sandboxId);
89
+ }
90
+
91
+ if (event) {
92
+ resultLogs = resultLogs.filter((log) => log && log.event === event);
93
+ }
94
+
95
+ if (!resultLogs || resultLogs.length === 0) {
96
+ // eslint-disable-next-line no-console
97
+ console.log('No logs found. (Check ~/.rock/logs/rock-cli-YYYY-MM-DD.log)');
98
+ return;
99
+ }
100
+
101
+ displayLogs(resultLogs, false, logFormat, columns, truncate, keyword, highlight);
102
+ }
103
+
104
+ module.exports = {
105
+ handleLogSearch,
106
+ };