rl-rockcli 0.0.14 → 0.0.16
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/README.md +2 -74
- package/commands/attach/ink-repl/InkREPL.js +19 -0
- package/commands/attach/ink-repl/builtinCommands.js +29 -86
- package/commands/attach/opentui-repl/App.tsx +15 -1
- package/commands/attach/opentui-repl/builtinCommands.ts +12 -0
- package/commands/attach/repl.js +1 -1
- package/commands/sandbox.js +29 -14
- package/help/renderer.js +55 -41
- package/package.json +1 -1
- package/utils/plugin-manager.js +78 -0
package/README.md
CHANGED
|
@@ -5,7 +5,6 @@ ROCK CLI 是一个开源的命令行工具,用于管理和操作沙箱环境
|
|
|
5
5
|
## 功能特性
|
|
6
6
|
|
|
7
7
|
- **沙箱管理** - 启动、停止、执行、上传/下载文件、交互式 REPL
|
|
8
|
-
- **会话管理** - 持久化执行环境,支持环境变量继承
|
|
9
8
|
|
|
10
9
|
## 安装
|
|
11
10
|
|
|
@@ -28,9 +27,7 @@ rockcli <command> [options]
|
|
|
28
27
|
### 全局选项
|
|
29
28
|
|
|
30
29
|
- `--verbose, -v` - 启用详细日志输出
|
|
31
|
-
- `--config` - 配置文件路径(默认:~/.rock/settings.json)
|
|
32
30
|
- `--base-url` - 服务器地址(覆盖配置文件)
|
|
33
|
-
- `--api-key` - API 密钥(覆盖配置文件)
|
|
34
31
|
|
|
35
32
|
## 命令详解
|
|
36
33
|
|
|
@@ -161,9 +158,6 @@ rockcli sandbox <sandbox-id> session close --session my-session
|
|
|
161
158
|
```bash
|
|
162
159
|
# 进入交互式 REPL
|
|
163
160
|
rockcli sandbox <sandbox-id> attach
|
|
164
|
-
|
|
165
|
-
# 或使用旧语法
|
|
166
|
-
rockcli attach <sandbox-id>
|
|
167
161
|
```
|
|
168
162
|
|
|
169
163
|
REPL 模式支持:
|
|
@@ -186,20 +180,16 @@ REPL 模式支持:
|
|
|
186
180
|
```json
|
|
187
181
|
{
|
|
188
182
|
"sandbox": {
|
|
189
|
-
"base_url": "http://localhost:8080"
|
|
190
|
-
"api_key": "your-api-key"
|
|
183
|
+
"base_url": "http://localhost:8080"
|
|
191
184
|
}
|
|
192
185
|
}
|
|
193
186
|
```
|
|
194
187
|
|
|
195
188
|
## 环境变量
|
|
196
189
|
|
|
197
|
-
|
|
190
|
+
支持通过环境变量设置服务器地址:
|
|
198
191
|
|
|
199
192
|
```bash
|
|
200
|
-
# 设置 API Key
|
|
201
|
-
export ROCK_API_KEY=your-api-key
|
|
202
|
-
|
|
203
193
|
# 设置服务器地址
|
|
204
194
|
export ROCKCLI_BASE_URL=http://localhost:8080
|
|
205
195
|
|
|
@@ -212,68 +202,6 @@ rockcli sandbox <sandbox-id> status
|
|
|
212
202
|
|
|
213
203
|
优先级:命令行参数 > 环境变量 > 配置文件
|
|
214
204
|
|
|
215
|
-
## 配置管理 (config)
|
|
216
|
-
|
|
217
|
-
配置管理命令用于设置和查看本地配置参数。
|
|
218
|
-
|
|
219
|
-
### 基本语法
|
|
220
|
-
|
|
221
|
-
```bash
|
|
222
|
-
rockcli config [options]
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
### 选项说明
|
|
226
|
-
|
|
227
|
-
- `--api-key <key>` - 设置 API 密钥
|
|
228
|
-
- `--base-url <url>` - 设置服务器基础 URL
|
|
229
|
-
- `--list` - 显示当前配置(脱敏敏感值)
|
|
230
|
-
|
|
231
|
-
### 示例
|
|
232
|
-
|
|
233
|
-
```bash
|
|
234
|
-
# 设置 API Key 和服务器地址
|
|
235
|
-
rockcli config --api-key xxx --base-url http://localhost:8080
|
|
236
|
-
|
|
237
|
-
# 显示当前配置
|
|
238
|
-
rockcli config --list
|
|
239
|
-
|
|
240
|
-
# 仅设置 API Key,其他值保持不变
|
|
241
|
-
rockcli config --api-key xxx
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
## 开发指南
|
|
245
|
-
|
|
246
|
-
### 项目结构
|
|
247
|
-
|
|
248
|
-
```
|
|
249
|
-
opensource/
|
|
250
|
-
├── bin/
|
|
251
|
-
│ └── rockcli.js # CLI 主入口
|
|
252
|
-
└── package.json # 包配置
|
|
253
|
-
|
|
254
|
-
(引用上层目录的共享代码)
|
|
255
|
-
├── commands/ # 命令模块
|
|
256
|
-
├── sdks/ # SDK 客户端
|
|
257
|
-
└── utils/ # 工具类
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
### 本地开发
|
|
261
|
-
|
|
262
|
-
```bash
|
|
263
|
-
# 克隆项目
|
|
264
|
-
git clone <repository-url>
|
|
265
|
-
|
|
266
|
-
# 安装依赖
|
|
267
|
-
npm install
|
|
268
|
-
|
|
269
|
-
# 本地链接
|
|
270
|
-
npm link
|
|
271
|
-
|
|
272
|
-
# 测试命令
|
|
273
|
-
rockcli --help
|
|
274
|
-
rockcli sandbox --help
|
|
275
|
-
```
|
|
276
|
-
|
|
277
205
|
## 相关资源
|
|
278
206
|
|
|
279
207
|
- GitHub: https://github.com/your-org/rock-cli
|
|
@@ -99,6 +99,7 @@ import {
|
|
|
99
99
|
isBuiltinCommand,
|
|
100
100
|
executeBuiltinCommand,
|
|
101
101
|
checkInteractiveCommand,
|
|
102
|
+
mergePluginAttachCommands,
|
|
102
103
|
} from './builtinCommands.js';
|
|
103
104
|
import { initConsoleLogger, clearConsoleLogger, consoleLogger } from './utils/consoleLogger.js';
|
|
104
105
|
import { copyToClipboard } from './utils/clipboard.js';
|
|
@@ -122,6 +123,24 @@ export function InkREPL({
|
|
|
122
123
|
const { exit } = useApp();
|
|
123
124
|
const { stdout } = useStdout();
|
|
124
125
|
|
|
126
|
+
// Merge plugin attach commands on mount
|
|
127
|
+
const hasMergedPluginCommands = useRef(false);
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
if (hasMergedPluginCommands.current) return;
|
|
130
|
+
hasMergedPluginCommands.current = true;
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const PluginManager = require('../../../utils/plugin-manager');
|
|
134
|
+
const pluginManager = PluginManager.getInstance();
|
|
135
|
+
const attachCommands = pluginManager.getRegisteredAttachCommands();
|
|
136
|
+
if (attachCommands.length > 0) {
|
|
137
|
+
mergePluginAttachCommands(attachCommands);
|
|
138
|
+
}
|
|
139
|
+
} catch (e) {
|
|
140
|
+
// PluginManager not initialized, ignore
|
|
141
|
+
}
|
|
142
|
+
}, []);
|
|
143
|
+
|
|
125
144
|
// Load history outputs on mount
|
|
126
145
|
const [initialOutputs, setInitialOutputs] = useState(null);
|
|
127
146
|
const [initialCommandHistory, setInitialCommandHistory] = useState(null);
|
|
@@ -13,7 +13,6 @@ export const COMMAND_HELP = {
|
|
|
13
13
|
// Primary commands
|
|
14
14
|
status: 'Show sandbox status',
|
|
15
15
|
sessions: 'List attach sessions',
|
|
16
|
-
log: 'View sandbox logs. Usage: /log [-f file] [-k keyword] [-n lines]',
|
|
17
16
|
tail: 'View last lines of a file. Usage: /tail [-n lines] [-k keyword] [-f] <file>',
|
|
18
17
|
upload: 'Upload file/directory to sandbox. Usage: /upload @<local> @<remote>',
|
|
19
18
|
download: 'Download file from sandbox. Usage: /download @<remote> @<local>',
|
|
@@ -379,91 +378,6 @@ export const commandHandlers = {
|
|
|
379
378
|
}
|
|
380
379
|
},
|
|
381
380
|
|
|
382
|
-
/**
|
|
383
|
-
* /log - View sandbox logs
|
|
384
|
-
*/
|
|
385
|
-
log: async (ctx, args) => {
|
|
386
|
-
let keyword = null;
|
|
387
|
-
let lines = 100;
|
|
388
|
-
let logFile = 'command.log';
|
|
389
|
-
|
|
390
|
-
for (let i = 0; i < args.length; i++) {
|
|
391
|
-
if (args[i] === '-k' && args[i + 1]) {
|
|
392
|
-
keyword = args[++i];
|
|
393
|
-
} else if (args[i] === '-n' && args[i + 1]) {
|
|
394
|
-
lines = clampNumber(args[++i], { min: 1, max: 5000, fallback: 100 });
|
|
395
|
-
} else if ((args[i] === '-f' || args[i] === '--file') && args[i + 1]) {
|
|
396
|
-
logFile = args[++i];
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// Reject obvious shell injection for user-provided values. This is not an auto-triggered path,
|
|
401
|
-
// but we still keep it safe since it runs in the sandbox shell.
|
|
402
|
-
if (hasDangerousShellChars(logFile) || (keyword && hasDangerousShellChars(keyword))) {
|
|
403
|
-
return { output: 'Invalid log parameters.', exitCode: 1 };
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
try {
|
|
407
|
-
// Try Loghouse first (only supports safe basenames for logFile)
|
|
408
|
-
const safeLoghouseFile = isSafeLogFileName(logFile) ? logFile : null;
|
|
409
|
-
const handleLogSearch =
|
|
410
|
-
typeof ctx.handleLogSearch === 'function'
|
|
411
|
-
? ctx.handleLogSearch
|
|
412
|
-
: require('../../log/search').handleLogSearch;
|
|
413
|
-
|
|
414
|
-
const searchArgv = {
|
|
415
|
-
sandboxId: ctx.sandboxId,
|
|
416
|
-
logFile: safeLoghouseFile || 'command.log',
|
|
417
|
-
minutes: 60,
|
|
418
|
-
limit: lines,
|
|
419
|
-
keyword: keyword || undefined,
|
|
420
|
-
raw: false,
|
|
421
|
-
highlight: true,
|
|
422
|
-
highlightSandboxId: false,
|
|
423
|
-
truncate: 200,
|
|
424
|
-
};
|
|
425
|
-
|
|
426
|
-
// Capture console output
|
|
427
|
-
const output = [];
|
|
428
|
-
const originalLog = console.log;
|
|
429
|
-
const originalError = console.error;
|
|
430
|
-
|
|
431
|
-
console.log = (...args) => output.push(args.join(' '));
|
|
432
|
-
console.error = (...args) => output.push(args.join(' '));
|
|
433
|
-
|
|
434
|
-
try {
|
|
435
|
-
await handleLogSearch(searchArgv);
|
|
436
|
-
} finally {
|
|
437
|
-
console.log = originalLog;
|
|
438
|
-
console.error = originalError;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
if (output.length > 0) {
|
|
442
|
-
return { output: output.join('\n'), exitCode: 0 };
|
|
443
|
-
}
|
|
444
|
-
return { output: 'No logs found.', exitCode: 0 };
|
|
445
|
-
} catch (error) {
|
|
446
|
-
// Fallback to local log
|
|
447
|
-
try {
|
|
448
|
-
const remotePath = resolveRemoteLogPath(logFile);
|
|
449
|
-
if (hasDangerousShellChars(remotePath)) {
|
|
450
|
-
return { output: 'Invalid log file path.', exitCode: 1 };
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
let command = `tail -n ${lines} -- ${shellEscapePosix(remotePath)} 2>/dev/null`;
|
|
454
|
-
if (keyword) {
|
|
455
|
-
command = `${command} | grep -i -- ${shellEscapePosix(keyword)}`;
|
|
456
|
-
}
|
|
457
|
-
command = `${command} || echo ${shellEscapePosix(keyword ? 'No matching logs' : 'No logs available')}`;
|
|
458
|
-
|
|
459
|
-
const result = await ctx.sessionManager.execute(command);
|
|
460
|
-
return { output: result.output || 'No logs available', exitCode: 0 };
|
|
461
|
-
} catch (e) {
|
|
462
|
-
return { output: `Failed to get logs: ${e.message}`, exitCode: 1 };
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
},
|
|
466
|
-
|
|
467
381
|
/**
|
|
468
382
|
* /upload - Upload file or directory to sandbox
|
|
469
383
|
* Supports @ prefix for paths (e.g., /upload @local.txt @/remote.txt)
|
|
@@ -1251,3 +1165,32 @@ export function checkInteractiveCommand(command) {
|
|
|
1251
1165
|
|
|
1252
1166
|
return { isInteractive: false, cmdName, alternative: null };
|
|
1253
1167
|
}
|
|
1168
|
+
|
|
1169
|
+
/**
|
|
1170
|
+
* Merge plugin attach commands into builtin commands
|
|
1171
|
+
* @param {Array<Object>} pluginCommands - Array of plugin command definitions
|
|
1172
|
+
* @param {string} pluginCommands[].name - Command name (without / prefix)
|
|
1173
|
+
* @param {string} pluginCommands[].description - Command description
|
|
1174
|
+
* @param {Function} pluginCommands[].handler - Command handler function
|
|
1175
|
+
*/
|
|
1176
|
+
export function mergePluginAttachCommands(pluginCommands) {
|
|
1177
|
+
if (!pluginCommands || !Array.isArray(pluginCommands)) {
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
for (const cmd of pluginCommands) {
|
|
1182
|
+
if (!cmd || !cmd.name) {
|
|
1183
|
+
continue;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// Merge into COMMAND_HELP
|
|
1187
|
+
if (cmd.description) {
|
|
1188
|
+
COMMAND_HELP[cmd.name] = cmd.description;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// Merge into commandHandlers
|
|
1192
|
+
if (cmd.handler && typeof cmd.handler === 'function') {
|
|
1193
|
+
commandHandlers[cmd.name] = cmd.handler;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
@@ -15,7 +15,7 @@ import { Console } from './components/Console.tsx';
|
|
|
15
15
|
import { Toast } from './components/Toast.tsx';
|
|
16
16
|
import { ConnectingScreen } from './components/ConnectingScreen.tsx';
|
|
17
17
|
import { useResources } from './hooks/useResources.ts';
|
|
18
|
-
import { executeBuiltinCommand, isBuiltinCommand, checkInteractiveCommand } from './builtinCommands.ts';
|
|
18
|
+
import { executeBuiltinCommand, isBuiltinCommand, checkInteractiveCommand, mergePluginAttachCommands } from './builtinCommands.ts';
|
|
19
19
|
import {
|
|
20
20
|
parseAtContext,
|
|
21
21
|
getLocalCompletions,
|
|
@@ -76,6 +76,20 @@ function ReplShell(props: { onExit?: () => void }) {
|
|
|
76
76
|
// Stats tracking
|
|
77
77
|
const stats = { startTime: Date.now(), shellCommands: 0, builtinCommands: 0 };
|
|
78
78
|
|
|
79
|
+
// Merge plugin attach commands on mount
|
|
80
|
+
onMount(() => {
|
|
81
|
+
try {
|
|
82
|
+
const PluginManager = require('../../../utils/plugin-manager');
|
|
83
|
+
const pluginManager = PluginManager.getInstance();
|
|
84
|
+
const attachCommands = pluginManager.getRegisteredAttachCommands();
|
|
85
|
+
if (attachCommands.length > 0) {
|
|
86
|
+
mergePluginAttachCommands(attachCommands);
|
|
87
|
+
}
|
|
88
|
+
} catch (e) {
|
|
89
|
+
// PluginManager not initialized, ignore
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
79
93
|
// AbortController for cancelling execution
|
|
80
94
|
let abortController: AbortController | null = null;
|
|
81
95
|
|
|
@@ -78,3 +78,15 @@ export async function checkInteractiveCommand(
|
|
|
78
78
|
const mod = getBuiltinModule();
|
|
79
79
|
return mod.checkInteractiveCommand(command);
|
|
80
80
|
}
|
|
81
|
+
|
|
82
|
+
export interface AttachCommandDef {
|
|
83
|
+
name: string;
|
|
84
|
+
description: string;
|
|
85
|
+
handler: (context: BuiltinContext, args: string[]) => Promise<BuiltinResult>;
|
|
86
|
+
pluginName?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function mergePluginAttachCommands(commands: AttachCommandDef[]): void {
|
|
90
|
+
const mod = getBuiltinModule();
|
|
91
|
+
return mod.mergePluginAttachCommands(commands);
|
|
92
|
+
}
|
package/commands/attach/repl.js
CHANGED
|
@@ -20,9 +20,9 @@ const DEFAULT_OPEN_SOURCE_BASE_URL = 'http://127.0.0.1:8080';
|
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* List of all builtin commands available in the REPL
|
|
23
|
+
* Note: /log is registered by loghouse plugin via registerAttachCommand
|
|
23
24
|
*/
|
|
24
25
|
const BUILTIN_COMMANDS = [
|
|
25
|
-
{ name: 'log', description: 'View sandbox logs' },
|
|
26
26
|
{ name: 'upload', description: 'Upload file to sandbox' },
|
|
27
27
|
{ name: 'download', description: 'Download file from sandbox' },
|
|
28
28
|
{ name: 'status', description: 'Show sandbox status' },
|
package/commands/sandbox.js
CHANGED
|
@@ -69,7 +69,8 @@ const sandboxExports = module.exports = {
|
|
|
69
69
|
.option('api-key', {
|
|
70
70
|
describe: 'API key for authentication',
|
|
71
71
|
type: 'string',
|
|
72
|
-
group: 'Command Options:'
|
|
72
|
+
group: 'Command Options:',
|
|
73
|
+
hidden: isOpenSource // Hide in open source version
|
|
73
74
|
})
|
|
74
75
|
.option('base-url', {
|
|
75
76
|
describe: 'Base URL for sandbox service',
|
|
@@ -109,17 +110,20 @@ const sandboxExports = module.exports = {
|
|
|
109
110
|
alias: 'c',
|
|
110
111
|
describe: 'Cluster ID (sets X-Cluster header)',
|
|
111
112
|
type: 'string',
|
|
112
|
-
group: 'Command Options:'
|
|
113
|
+
group: 'Command Options:',
|
|
114
|
+
hidden: isOpenSource // Hide in open source version
|
|
113
115
|
})
|
|
114
116
|
.option('experiment-id', {
|
|
115
117
|
describe: 'Experiment ID (sets X-Experiment-Id header)',
|
|
116
118
|
type: 'string',
|
|
117
|
-
group: 'Command Options:'
|
|
119
|
+
group: 'Command Options:',
|
|
120
|
+
hidden: isOpenSource // Hide in open source version
|
|
118
121
|
})
|
|
119
122
|
.option('user-id', {
|
|
120
123
|
describe: 'User ID (sets X-User-Id header)',
|
|
121
124
|
type: 'string',
|
|
122
|
-
group: 'Command Options:'
|
|
125
|
+
group: 'Command Options:',
|
|
126
|
+
hidden: isOpenSource // Hide in open source version
|
|
123
127
|
})
|
|
124
128
|
.option('extra-header', {
|
|
125
129
|
alias: 'H',
|
|
@@ -147,68 +151,79 @@ const sandboxExports = module.exports = {
|
|
|
147
151
|
default: false,
|
|
148
152
|
group: 'Command Options:'
|
|
149
153
|
})
|
|
154
|
+
// Log Options - hidden in open source version (log command not supported)
|
|
150
155
|
.option('n', {
|
|
151
156
|
alias: 'lines',
|
|
152
157
|
describe: 'Number of recent log lines to display (default: 10)',
|
|
153
158
|
type: 'number',
|
|
154
159
|
default: 10,
|
|
155
|
-
group: 'Log Options:'
|
|
160
|
+
group: 'Log Options:',
|
|
161
|
+
hidden: isOpenSource
|
|
156
162
|
})
|
|
157
163
|
.option('f', {
|
|
158
164
|
alias: 'follow',
|
|
159
165
|
describe: 'Follow mode: continuously monitor logs (like tail -f)',
|
|
160
166
|
type: 'boolean',
|
|
161
167
|
default: false,
|
|
162
|
-
group: 'Log Options:'
|
|
168
|
+
group: 'Log Options:',
|
|
169
|
+
hidden: isOpenSource
|
|
163
170
|
})
|
|
164
171
|
.option('sleep-interval', {
|
|
165
172
|
alias: 'i',
|
|
166
173
|
describe: 'Refresh interval in seconds for follow mode (default: 5)',
|
|
167
174
|
type: 'number',
|
|
168
175
|
default: 5,
|
|
169
|
-
group: 'Log Options:'
|
|
176
|
+
group: 'Log Options:',
|
|
177
|
+
hidden: isOpenSource
|
|
170
178
|
})
|
|
171
179
|
.option('q', {
|
|
172
180
|
alias: 'quiet',
|
|
173
181
|
describe: 'Quiet mode: hide internal fields (timestamp, time_iso8601, etc.)',
|
|
174
182
|
type: 'boolean',
|
|
175
183
|
default: false,
|
|
176
|
-
group: 'Log Options:'
|
|
184
|
+
group: 'Log Options:',
|
|
185
|
+
hidden: isOpenSource
|
|
177
186
|
})
|
|
178
187
|
.option('keyword', {
|
|
179
188
|
alias: 'k',
|
|
180
189
|
describe: 'Filter logs by keyword',
|
|
181
190
|
type: 'string',
|
|
182
|
-
group: 'Log Options:'
|
|
191
|
+
group: 'Log Options:',
|
|
192
|
+
hidden: isOpenSource
|
|
183
193
|
})
|
|
184
194
|
.option('log-file', {
|
|
185
195
|
describe: 'Filter by log file name',
|
|
186
196
|
type: 'string',
|
|
187
|
-
group: 'Log Options:'
|
|
197
|
+
group: 'Log Options:',
|
|
198
|
+
hidden: isOpenSource
|
|
188
199
|
})
|
|
189
200
|
.option('debug', {
|
|
190
201
|
describe: 'Enable debug mode to display SQL query',
|
|
191
202
|
type: 'boolean',
|
|
192
203
|
default: false,
|
|
193
|
-
group: 'Log Options:'
|
|
204
|
+
group: 'Log Options:',
|
|
205
|
+
hidden: isOpenSource
|
|
194
206
|
})
|
|
195
207
|
.option('raw', {
|
|
196
208
|
describe: 'Display raw output (flatten to single line)',
|
|
197
209
|
type: 'boolean',
|
|
198
210
|
default: false,
|
|
199
|
-
group: 'Log Options:'
|
|
211
|
+
group: 'Log Options:',
|
|
212
|
+
hidden: isOpenSource
|
|
200
213
|
})
|
|
201
214
|
.option('multilines', {
|
|
202
215
|
describe: 'Display multi-line logs with separators',
|
|
203
216
|
type: 'boolean',
|
|
204
217
|
default: false,
|
|
205
|
-
group: 'Log Options:'
|
|
218
|
+
group: 'Log Options:',
|
|
219
|
+
hidden: isOpenSource
|
|
206
220
|
})
|
|
207
221
|
.option('highlight', {
|
|
208
222
|
describe: 'Enable keyword highlighting (default: true)',
|
|
209
223
|
type: 'boolean',
|
|
210
224
|
default: true,
|
|
211
|
-
group: 'Log Options:'
|
|
225
|
+
group: 'Log Options:',
|
|
226
|
+
hidden: isOpenSource
|
|
212
227
|
})
|
|
213
228
|
.example([
|
|
214
229
|
['$0 sandbox start --image python:3.11', 'Start a new sandbox'],
|
package/help/renderer.js
CHANGED
|
@@ -36,24 +36,24 @@ function renderHelp(commandPath, pluginManager = null) {
|
|
|
36
36
|
// 检查是否是插件命令(带有 helpConfig)
|
|
37
37
|
if (pluginManager) {
|
|
38
38
|
const pluginCommands = pluginManager.getRegisteredCommands();
|
|
39
|
-
|
|
39
|
+
|
|
40
40
|
// 查找父命令(如 "log")
|
|
41
41
|
const parentCmd = pluginCommands.find(cmd => cmd.command === command);
|
|
42
|
-
|
|
42
|
+
|
|
43
43
|
if (parentCmd && parentCmd.helpConfig) {
|
|
44
44
|
// 使用插件的 helpConfig 渲染帮助
|
|
45
45
|
return renderPluginHelpConfig(command, parentCmd.helpConfig, subcommand, rest);
|
|
46
46
|
}
|
|
47
|
-
|
|
47
|
+
|
|
48
48
|
// 首先检查是否是插件一级命令(有子命令)
|
|
49
49
|
const hasSubCommands = pluginCommands.some(cmd => cmd.command.startsWith(command + ' '));
|
|
50
|
-
|
|
50
|
+
|
|
51
51
|
if (hasSubCommands && !subcommand) {
|
|
52
52
|
// 这是一个有子命令的父命令,显示子命令列表
|
|
53
53
|
const subCommands = pluginCommands.filter(cmd => cmd.command.startsWith(command + ' '));
|
|
54
54
|
return renderPluginSubcommandsHelp(command, subCommands, pluginManager);
|
|
55
55
|
}
|
|
56
|
-
|
|
56
|
+
|
|
57
57
|
// 检查是否是插件二级命令
|
|
58
58
|
if (hasSubCommands && subcommand) {
|
|
59
59
|
const fullCommand = `${command} ${subcommand}`;
|
|
@@ -66,11 +66,11 @@ function renderHelp(commandPath, pluginManager = null) {
|
|
|
66
66
|
return renderPluginCommandHelp(pluginCmd, pluginManager);
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
|
-
|
|
69
|
+
|
|
70
70
|
// 否则查找匹配的一级命令
|
|
71
71
|
// 优先查找精确匹配的父命令(如 "formula"),而不是子命令(如 "formula list")
|
|
72
72
|
let pluginCmd = pluginCommands.find(cmd => cmd.command === command);
|
|
73
|
-
|
|
73
|
+
|
|
74
74
|
// 如果没有精确匹配,再查找命令名称匹配的
|
|
75
75
|
if (!pluginCmd) {
|
|
76
76
|
pluginCmd = pluginCommands.find(cmd => {
|
|
@@ -134,6 +134,9 @@ function getAsciiLogo() {
|
|
|
134
134
|
* 渲染一级帮助(根帮助)
|
|
135
135
|
*/
|
|
136
136
|
function renderRootHelp(pluginManager = null) {
|
|
137
|
+
// Check if running in open source mode (evaluated at function call time)
|
|
138
|
+
const isOpenSource = process.env.ROCKCLI_MODE === 'opensource';
|
|
139
|
+
|
|
137
140
|
const lines = [];
|
|
138
141
|
|
|
139
142
|
// Logo (with leading empty line like opencode)
|
|
@@ -176,14 +179,14 @@ function renderRootHelp(pluginManager = null) {
|
|
|
176
179
|
if (pluginManager) {
|
|
177
180
|
const pluginCommands = pluginManager.getRegisteredCommands();
|
|
178
181
|
const existingCommands = helpConfig.groups.flatMap(g => g.commands);
|
|
179
|
-
|
|
182
|
+
|
|
180
183
|
// 按命令层级分组:一级命令和二级命令
|
|
181
184
|
const topLevelCommands = [];
|
|
182
185
|
const subCommands = new Map(); // key: parent command, value: sub commands list
|
|
183
|
-
|
|
186
|
+
|
|
184
187
|
for (const cmd of pluginCommands) {
|
|
185
188
|
const parts = cmd.command.split(' ');
|
|
186
|
-
|
|
189
|
+
|
|
187
190
|
// 判断是否是一级命令
|
|
188
191
|
// 如果只有一个部分,或者第二个部分以 [ 或 < 开头(参数),则是一级命令
|
|
189
192
|
if (parts.length === 1 || (parts.length === 2 && (parts[1].startsWith('[') || parts[1].startsWith('<')))) {
|
|
@@ -198,7 +201,7 @@ function renderRootHelp(pluginManager = null) {
|
|
|
198
201
|
subCommands.get(parent).push(cmd);
|
|
199
202
|
}
|
|
200
203
|
}
|
|
201
|
-
|
|
204
|
+
|
|
202
205
|
// 创建父命令的描述(用于显示)
|
|
203
206
|
// 优先使用插件注册的父命令描述
|
|
204
207
|
const parentCommandDescriptions = new Map();
|
|
@@ -214,20 +217,20 @@ function renderRootHelp(pluginManager = null) {
|
|
|
214
217
|
}
|
|
215
218
|
}
|
|
216
219
|
}
|
|
217
|
-
|
|
220
|
+
|
|
218
221
|
// 过滤掉与内置命令冲突的一级命令(排除纯父命令描述)
|
|
219
222
|
const filteredTopLevel = topLevelCommands.filter(cmd => {
|
|
220
223
|
const cmdName = cmd.command.split(' ')[0];
|
|
221
224
|
return !existingCommands.includes(cmdName);
|
|
222
225
|
});
|
|
223
|
-
|
|
226
|
+
|
|
224
227
|
// 创建一个包含所有一级命令名称的集合(包括有子命令的父命令)
|
|
225
228
|
const allTopLevelNames = new Set(filteredTopLevel.map(cmd => cmd.command.split(' ')[0]));
|
|
226
229
|
subCommands.forEach((_, parent) => allTopLevelNames.add(parent));
|
|
227
|
-
|
|
230
|
+
|
|
228
231
|
if (allTopLevelNames.size > 0) {
|
|
229
232
|
lines.push('插件命令');
|
|
230
|
-
|
|
233
|
+
|
|
231
234
|
// 按名称排序并显示所有一级命令
|
|
232
235
|
const sortedNames = Array.from(allTopLevelNames).sort();
|
|
233
236
|
for (const name of sortedNames) {
|
|
@@ -242,7 +245,7 @@ function renderRootHelp(pluginManager = null) {
|
|
|
242
245
|
lines.push(INDENT + padEnd(name, CMD_WIDTH) + desc);
|
|
243
246
|
}
|
|
244
247
|
}
|
|
245
|
-
|
|
248
|
+
|
|
246
249
|
lines.push('');
|
|
247
250
|
}
|
|
248
251
|
}
|
|
@@ -250,7 +253,11 @@ function renderRootHelp(pluginManager = null) {
|
|
|
250
253
|
// 全局选项
|
|
251
254
|
lines.push('全局选项');
|
|
252
255
|
for (const opt of helpConfig.globalOptions) {
|
|
253
|
-
|
|
256
|
+
// Hide --api-key and --cluster in open source version
|
|
257
|
+
if (isOpenSource && (opt.flags.includes('--api-key') || opt.flags.includes('--cluster'))) {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
const defaultStr = opt.default ? ` (默认:${opt.default})` : '';
|
|
254
261
|
lines.push(INDENT + padEnd(opt.flags, FLAG_WIDTH) + opt.description + defaultStr);
|
|
255
262
|
}
|
|
256
263
|
lines.push('');
|
|
@@ -275,9 +282,12 @@ function renderRootHelp(pluginManager = null) {
|
|
|
275
282
|
* 渲染命令帮助(二级)
|
|
276
283
|
*/
|
|
277
284
|
function renderCommandHelp(command) {
|
|
285
|
+
// Check if running in open source mode (evaluated at function call time)
|
|
286
|
+
const isOpenSource = process.env.ROCKCLI_MODE === 'opensource';
|
|
287
|
+
|
|
278
288
|
const cmdConfig = helpConfig.commands[command];
|
|
279
289
|
if (!cmdConfig) {
|
|
280
|
-
return
|
|
290
|
+
return `未知命令:${command}`;
|
|
281
291
|
}
|
|
282
292
|
|
|
283
293
|
const lines = [];
|
|
@@ -302,7 +312,7 @@ function renderCommandHelp(command) {
|
|
|
302
312
|
if (cmdConfig.subcommands) {
|
|
303
313
|
lines.push('子命令');
|
|
304
314
|
for (const [name, subcmd] of Object.entries(cmdConfig.subcommands)) {
|
|
305
|
-
const aliasStr = subcmd.aliases ? ` (
|
|
315
|
+
const aliasStr = subcmd.aliases ? ` (别名:${subcmd.aliases.join(', ')})` : '';
|
|
306
316
|
lines.push(INDENT + padEnd(name, CMD_WIDTH) + subcmd.description + aliasStr);
|
|
307
317
|
}
|
|
308
318
|
lines.push('');
|
|
@@ -312,13 +322,17 @@ function renderCommandHelp(command) {
|
|
|
312
322
|
if (cmdConfig.commonOptions && cmdConfig.commonOptions.length > 0) {
|
|
313
323
|
lines.push('通用选项');
|
|
314
324
|
for (const opt of cmdConfig.commonOptions) {
|
|
315
|
-
|
|
325
|
+
// Hide --api-key and --cluster in open source version
|
|
326
|
+
if (isOpenSource && (opt.flags.includes('--api-key') || opt.flags.includes('--cluster'))) {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
const defaultStr = opt.default ? ` (默认:${opt.default})` : '';
|
|
316
330
|
const requiredStr = opt.required ? ' (必填)' : '';
|
|
317
331
|
lines.push(INDENT + padEnd(opt.flags, FLAG_WIDTH) + opt.description + defaultStr + requiredStr);
|
|
318
332
|
}
|
|
319
333
|
lines.push('');
|
|
320
334
|
}
|
|
321
|
-
|
|
335
|
+
|
|
322
336
|
// 选项 (适用于无子命令的命令,如 config)
|
|
323
337
|
if (cmdConfig.options && cmdConfig.options.length > 0) {
|
|
324
338
|
lines.push('选项');
|
|
@@ -481,11 +495,11 @@ function renderPluginSubcommandsHelp(command, subCommands, pluginManager) {
|
|
|
481
495
|
// 有真正的子命令,显示子命令列表
|
|
482
496
|
lines.push(parentDescribe);
|
|
483
497
|
lines.push('');
|
|
484
|
-
|
|
498
|
+
|
|
485
499
|
lines.push('用法');
|
|
486
500
|
lines.push(INDENT + `rockcli ${command} <子命令> [选项]`);
|
|
487
501
|
lines.push('');
|
|
488
|
-
|
|
502
|
+
|
|
489
503
|
lines.push('子命令');
|
|
490
504
|
for (const subCmd of subCommands) {
|
|
491
505
|
const parts = subCmd.command.split(' ');
|
|
@@ -496,7 +510,7 @@ function renderPluginSubcommandsHelp(command, subCommands, pluginManager) {
|
|
|
496
510
|
}
|
|
497
511
|
}
|
|
498
512
|
lines.push('');
|
|
499
|
-
|
|
513
|
+
|
|
500
514
|
lines.push('来源');
|
|
501
515
|
lines.push(INDENT + `插件: ${pluginName}`);
|
|
502
516
|
lines.push('');
|
|
@@ -507,7 +521,7 @@ function renderPluginSubcommandsHelp(command, subCommands, pluginManager) {
|
|
|
507
521
|
lines.push(INDENT + `rockcli ${subCmd.command}`);
|
|
508
522
|
});
|
|
509
523
|
lines.push('');
|
|
510
|
-
|
|
524
|
+
|
|
511
525
|
// 显示第一个参数变体的选项
|
|
512
526
|
if (subCommands[0].options) {
|
|
513
527
|
lines.push('选项');
|
|
@@ -518,7 +532,7 @@ function renderPluginSubcommandsHelp(command, subCommands, pluginManager) {
|
|
|
518
532
|
}
|
|
519
533
|
lines.push('');
|
|
520
534
|
}
|
|
521
|
-
|
|
535
|
+
|
|
522
536
|
lines.push('来源');
|
|
523
537
|
lines.push(INDENT + `插件: ${pluginName}`);
|
|
524
538
|
lines.push('');
|
|
@@ -533,12 +547,12 @@ function renderPluginSubcommandsHelp(command, subCommands, pluginManager) {
|
|
|
533
547
|
function renderPluginCommandHelp(cmd, pluginManager = null) {
|
|
534
548
|
const lines = [];
|
|
535
549
|
const parts = cmd.command.split(' ');
|
|
536
|
-
|
|
550
|
+
|
|
537
551
|
// 检查是否是父命令(有子命令)
|
|
538
552
|
if (pluginManager && parts.length === 1) {
|
|
539
553
|
const allPluginCommands = pluginManager.getRegisteredCommands();
|
|
540
554
|
const relatedCommands = allPluginCommands.filter(c => c.command.startsWith(cmd.command + ' '));
|
|
541
|
-
|
|
555
|
+
|
|
542
556
|
if (relatedCommands.length > 0) {
|
|
543
557
|
// 检查这些相关命令的第二个部分是否都是参数标记(子命令还是参数变体)
|
|
544
558
|
const hasRealSubcommands = relatedCommands.some(c => {
|
|
@@ -548,16 +562,16 @@ function renderPluginCommandHelp(cmd, pluginManager = null) {
|
|
|
548
562
|
// 如果第二个部分以 [ 或 < 开头,则是参数变体,不是子命令
|
|
549
563
|
return !secondPart.startsWith('[') && !secondPart.startsWith('<');
|
|
550
564
|
});
|
|
551
|
-
|
|
565
|
+
|
|
552
566
|
if (hasRealSubcommands) {
|
|
553
567
|
// 有真正的子命令,显示子命令列表
|
|
554
568
|
lines.push(cmd.describe || '插件命令');
|
|
555
569
|
lines.push('');
|
|
556
|
-
|
|
570
|
+
|
|
557
571
|
lines.push('用法');
|
|
558
572
|
lines.push(INDENT + `rockcli ${cmd.command} <子命令> [选项]`);
|
|
559
573
|
lines.push('');
|
|
560
|
-
|
|
574
|
+
|
|
561
575
|
lines.push('子命令');
|
|
562
576
|
for (const subCmd of relatedCommands) {
|
|
563
577
|
const parts = subCmd.command.split(' ');
|
|
@@ -568,13 +582,13 @@ function renderPluginCommandHelp(cmd, pluginManager = null) {
|
|
|
568
582
|
}
|
|
569
583
|
}
|
|
570
584
|
lines.push('');
|
|
571
|
-
|
|
585
|
+
|
|
572
586
|
if (cmd.pluginName) {
|
|
573
587
|
lines.push('来源');
|
|
574
588
|
lines.push(INDENT + `插件: ${cmd.pluginName}`);
|
|
575
589
|
lines.push('');
|
|
576
590
|
}
|
|
577
|
-
|
|
591
|
+
|
|
578
592
|
return lines.join('\n');
|
|
579
593
|
} else {
|
|
580
594
|
// 没有真正的子命令,都是参数变体,显示命令列表
|
|
@@ -583,7 +597,7 @@ function renderPluginCommandHelp(cmd, pluginManager = null) {
|
|
|
583
597
|
lines.push(INDENT + `rockcli ${relCmd.command}`);
|
|
584
598
|
});
|
|
585
599
|
lines.push('');
|
|
586
|
-
|
|
600
|
+
|
|
587
601
|
// 显示第一个参数变体的选项
|
|
588
602
|
if (relatedCommands[0].options) {
|
|
589
603
|
lines.push('选项');
|
|
@@ -594,18 +608,18 @@ function renderPluginCommandHelp(cmd, pluginManager = null) {
|
|
|
594
608
|
}
|
|
595
609
|
lines.push('');
|
|
596
610
|
}
|
|
597
|
-
|
|
611
|
+
|
|
598
612
|
if (cmd.pluginName) {
|
|
599
613
|
lines.push('来源');
|
|
600
614
|
lines.push(INDENT + `插件: ${cmd.pluginName}`);
|
|
601
615
|
lines.push('');
|
|
602
616
|
}
|
|
603
|
-
|
|
617
|
+
|
|
604
618
|
return lines.join('\n');
|
|
605
619
|
}
|
|
606
620
|
}
|
|
607
621
|
}
|
|
608
|
-
|
|
622
|
+
|
|
609
623
|
// 单个命令或没有子命令的情况
|
|
610
624
|
lines.push(cmd.describe || '插件命令');
|
|
611
625
|
lines.push('');
|
|
@@ -648,19 +662,19 @@ function renderPluginHelpConfig(command, helpConfig, subcommand, rest) {
|
|
|
648
662
|
// 渲染父命令帮助
|
|
649
663
|
return renderPluginCommandHelpConfig(command, helpConfig);
|
|
650
664
|
}
|
|
651
|
-
|
|
665
|
+
|
|
652
666
|
// 渲染子命令帮助
|
|
653
667
|
if (helpConfig.subcommands && helpConfig.subcommands[subcommand]) {
|
|
654
668
|
const subcmdConfig = helpConfig.subcommands[subcommand];
|
|
655
|
-
|
|
669
|
+
|
|
656
670
|
// 检查是否有更深层的嵌套子命令
|
|
657
671
|
if (rest.length > 0 && subcmdConfig.subcommands && subcmdConfig.subcommands[rest[0]]) {
|
|
658
672
|
return renderPluginNestedSubcommandHelpConfig(command, subcommand, rest[0], helpConfig);
|
|
659
673
|
}
|
|
660
|
-
|
|
674
|
+
|
|
661
675
|
return renderPluginSubcommandHelpConfig(command, subcommand, subcmdConfig);
|
|
662
676
|
}
|
|
663
|
-
|
|
677
|
+
|
|
664
678
|
// 没有找到子命令配置,回退到父命令
|
|
665
679
|
return renderPluginCommandHelpConfig(command, helpConfig);
|
|
666
680
|
}
|
package/package.json
CHANGED
package/utils/plugin-manager.js
CHANGED
|
@@ -17,6 +17,12 @@ class PluginManager {
|
|
|
17
17
|
this._resolver = new DependencyResolver();
|
|
18
18
|
this._loadedPlugins = [];
|
|
19
19
|
this._registeredCommands = [];
|
|
20
|
+
this._registeredAttachCommands = [];
|
|
21
|
+
this._builtInAttachCommands = [
|
|
22
|
+
'status', 'sessions', 'tail', 'upload', 'download',
|
|
23
|
+
'stop', 'clear', 'stats', 'copy', 'about', 'retry', 'theme',
|
|
24
|
+
'cleanup-history', 'help', 'docs', 'bug', 'close', 'exit', 'quit', 'resume'
|
|
25
|
+
];
|
|
20
26
|
this._currentPluginMetadata = null;
|
|
21
27
|
}
|
|
22
28
|
|
|
@@ -309,6 +315,36 @@ class PluginManager {
|
|
|
309
315
|
* @param {number} code - 退出码
|
|
310
316
|
*/
|
|
311
317
|
gracefulExit,
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* 注册 attach 内置命令
|
|
321
|
+
* @param {Object} commandDef - 命令定义
|
|
322
|
+
* @param {string} commandDef.name - 命令名称(不含 / 前缀)
|
|
323
|
+
* @param {string} commandDef.description - 命令描述
|
|
324
|
+
* @param {Function} commandDef.handler - 命令处理函数 (ctx, args) => Promise<{output, exitCode}>
|
|
325
|
+
*/
|
|
326
|
+
registerAttachCommand: (commandDef) => {
|
|
327
|
+
// 检查是否覆盖内置命令
|
|
328
|
+
if (this._builtInAttachCommands.includes(commandDef.name)) {
|
|
329
|
+
throw new Error(
|
|
330
|
+
`Cannot override built-in attach command: ${commandDef.name}`
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// 检查命令冲突
|
|
335
|
+
if (this._isAttachCommandRegistered(commandDef.name)) {
|
|
336
|
+
throw new Error(
|
|
337
|
+
`Attach command "${commandDef.name}" is already registered`
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
this._registeredAttachCommands.push({
|
|
342
|
+
...commandDef,
|
|
343
|
+
pluginName
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
logger.debug(`Attach command registered by plugin ${pluginName}: /${commandDef.name}`);
|
|
347
|
+
},
|
|
312
348
|
};
|
|
313
349
|
}
|
|
314
350
|
|
|
@@ -328,6 +364,22 @@ class PluginManager {
|
|
|
328
364
|
return [...this._registeredCommands];
|
|
329
365
|
}
|
|
330
366
|
|
|
367
|
+
/**
|
|
368
|
+
* 检查 attach 命令是否已注册
|
|
369
|
+
* @private
|
|
370
|
+
*/
|
|
371
|
+
_isAttachCommandRegistered(name) {
|
|
372
|
+
return this._registeredAttachCommands.some(cmd => cmd.name === name);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* 获取所有插件注册的 attach 命令
|
|
377
|
+
* @returns {Array<Object>}
|
|
378
|
+
*/
|
|
379
|
+
getRegisteredAttachCommands() {
|
|
380
|
+
return [...this._registeredAttachCommands];
|
|
381
|
+
}
|
|
382
|
+
|
|
331
383
|
/**
|
|
332
384
|
* 获取所有已加载的插件信息
|
|
333
385
|
* @returns {Array<Object>}
|
|
@@ -345,4 +397,30 @@ class PluginManager {
|
|
|
345
397
|
}
|
|
346
398
|
}
|
|
347
399
|
|
|
400
|
+
/**
|
|
401
|
+
* 单例实例
|
|
402
|
+
* @type {PluginManager|null}
|
|
403
|
+
* @private
|
|
404
|
+
*/
|
|
405
|
+
PluginManager._instance = null;
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* 获取 PluginManager 单例实例
|
|
409
|
+
* @returns {PluginManager}
|
|
410
|
+
*/
|
|
411
|
+
PluginManager.getInstance = function() {
|
|
412
|
+
if (!PluginManager._instance) {
|
|
413
|
+
throw new Error('PluginManager not initialized. Call setInstance() first.');
|
|
414
|
+
}
|
|
415
|
+
return PluginManager._instance;
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* 设置 PluginManager 单例实例
|
|
420
|
+
* @param {PluginManager} instance
|
|
421
|
+
*/
|
|
422
|
+
PluginManager.setInstance = function(instance) {
|
|
423
|
+
PluginManager._instance = instance;
|
|
424
|
+
};
|
|
425
|
+
|
|
348
426
|
module.exports = PluginManager;
|