@runloop/rl-cli 0.0.1 → 0.0.2
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/dist/commands/devbox/create.js +1 -2
- package/dist/commands/devbox/list.js +231 -46
- package/dist/components/ActionsPopup.js +42 -0
- package/dist/components/DevboxActionsMenu.js +460 -0
- package/dist/components/DevboxCreatePage.js +15 -7
- package/dist/components/DevboxDetailPage.js +54 -362
- package/dist/components/StatusBadge.js +15 -12
- package/package.json +2 -1
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Box, Text, useInput, useApp, useStdout } from 'ink';
|
|
4
|
+
import TextInput from 'ink-text-input';
|
|
5
|
+
import figures from 'figures';
|
|
6
|
+
import { getClient } from '../utils/client.js';
|
|
7
|
+
import { Header } from './Header.js';
|
|
8
|
+
import { SpinnerComponent } from './Spinner.js';
|
|
9
|
+
import { ErrorMessage } from './ErrorMessage.js';
|
|
10
|
+
import { SuccessMessage } from './SuccessMessage.js';
|
|
11
|
+
import { Breadcrumb } from './Breadcrumb.js';
|
|
12
|
+
export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
13
|
+
{ label: 'Devboxes' },
|
|
14
|
+
{ label: devbox.name || devbox.id, active: true }
|
|
15
|
+
], initialOperation, initialOperationIndex = 0, }) => {
|
|
16
|
+
const { exit } = useApp();
|
|
17
|
+
const { stdout } = useStdout();
|
|
18
|
+
const [loading, setLoading] = React.useState(false);
|
|
19
|
+
const [selectedOperation, setSelectedOperation] = React.useState(initialOperationIndex);
|
|
20
|
+
const [executingOperation, setExecutingOperation] = React.useState(initialOperation || null);
|
|
21
|
+
const [operationInput, setOperationInput] = React.useState('');
|
|
22
|
+
const [operationResult, setOperationResult] = React.useState(null);
|
|
23
|
+
const [operationError, setOperationError] = React.useState(null);
|
|
24
|
+
const [logsWrapMode, setLogsWrapMode] = React.useState(true);
|
|
25
|
+
const [logsScroll, setLogsScroll] = React.useState(0);
|
|
26
|
+
const [execScroll, setExecScroll] = React.useState(0);
|
|
27
|
+
const [copyStatus, setCopyStatus] = React.useState(null);
|
|
28
|
+
const allOperations = [
|
|
29
|
+
{ key: 'logs', label: 'View Logs', color: 'blue', icon: figures.info, shortcut: 'l' },
|
|
30
|
+
{ key: 'exec', label: 'Execute Command', color: 'green', icon: figures.play, shortcut: 'e' },
|
|
31
|
+
{ key: 'upload', label: 'Upload File', color: 'green', icon: figures.arrowUp, shortcut: 'u' },
|
|
32
|
+
{ key: 'snapshot', label: 'Create Snapshot', color: 'yellow', icon: figures.circleFilled, shortcut: 'n' },
|
|
33
|
+
{ key: 'ssh', label: 'SSH onto the box', color: 'cyan', icon: figures.arrowRight, shortcut: 's' },
|
|
34
|
+
{ key: 'tunnel', label: 'Open Tunnel', color: 'magenta', icon: figures.pointerSmall, shortcut: 't' },
|
|
35
|
+
{ key: 'suspend', label: 'Suspend Devbox', color: 'yellow', icon: figures.squareSmallFilled, shortcut: 'p' },
|
|
36
|
+
{ key: 'resume', label: 'Resume Devbox', color: 'green', icon: figures.play, shortcut: 'r' },
|
|
37
|
+
{ key: 'delete', label: 'Shutdown Devbox', color: 'red', icon: figures.cross, shortcut: 'd' },
|
|
38
|
+
];
|
|
39
|
+
// Filter operations based on devbox status
|
|
40
|
+
const operations = devbox ? allOperations.filter(op => {
|
|
41
|
+
const status = devbox.status;
|
|
42
|
+
// When suspended: logs and resume
|
|
43
|
+
if (status === 'suspended') {
|
|
44
|
+
return op.key === 'resume' || op.key === 'logs';
|
|
45
|
+
}
|
|
46
|
+
// When not running (shutdown, failure, etc): only logs
|
|
47
|
+
if (status !== 'running' && status !== 'provisioning' && status !== 'initializing') {
|
|
48
|
+
return op.key === 'logs';
|
|
49
|
+
}
|
|
50
|
+
// When running: everything except resume
|
|
51
|
+
if (status === 'running') {
|
|
52
|
+
return op.key !== 'resume';
|
|
53
|
+
}
|
|
54
|
+
// Default for transitional states (provisioning, initializing)
|
|
55
|
+
return op.key === 'logs' || op.key === 'delete';
|
|
56
|
+
}) : allOperations;
|
|
57
|
+
// Auto-execute operations that don't need input
|
|
58
|
+
React.useEffect(() => {
|
|
59
|
+
if ((executingOperation === 'delete' || executingOperation === 'ssh' || executingOperation === 'logs' || executingOperation === 'suspend' || executingOperation === 'resume') && !loading && devbox) {
|
|
60
|
+
executeOperation();
|
|
61
|
+
}
|
|
62
|
+
}, [executingOperation]);
|
|
63
|
+
useInput((input, key) => {
|
|
64
|
+
// Handle operation input mode
|
|
65
|
+
if (executingOperation && !operationResult && !operationError) {
|
|
66
|
+
if (key.return && operationInput.trim()) {
|
|
67
|
+
executeOperation();
|
|
68
|
+
}
|
|
69
|
+
else if (input === 'q' || key.escape) {
|
|
70
|
+
console.clear();
|
|
71
|
+
setExecutingOperation(null);
|
|
72
|
+
setOperationInput('');
|
|
73
|
+
}
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
// Handle operation result display
|
|
77
|
+
if (operationResult || operationError) {
|
|
78
|
+
if (input === 'q' || key.escape || key.return) {
|
|
79
|
+
console.clear();
|
|
80
|
+
setOperationResult(null);
|
|
81
|
+
setOperationError(null);
|
|
82
|
+
setExecutingOperation(null);
|
|
83
|
+
setOperationInput('');
|
|
84
|
+
setLogsWrapMode(true);
|
|
85
|
+
setLogsScroll(0);
|
|
86
|
+
setExecScroll(0);
|
|
87
|
+
setCopyStatus(null);
|
|
88
|
+
}
|
|
89
|
+
else if ((key.upArrow || input === 'k') && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'exec') {
|
|
90
|
+
setExecScroll(Math.max(0, execScroll - 1));
|
|
91
|
+
}
|
|
92
|
+
else if ((key.downArrow || input === 'j') && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'exec') {
|
|
93
|
+
setExecScroll(execScroll + 1);
|
|
94
|
+
}
|
|
95
|
+
else if (key.pageUp && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'exec') {
|
|
96
|
+
setExecScroll(Math.max(0, execScroll - 10));
|
|
97
|
+
}
|
|
98
|
+
else if (key.pageDown && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'exec') {
|
|
99
|
+
setExecScroll(execScroll + 10);
|
|
100
|
+
}
|
|
101
|
+
else if (input === 'g' && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'exec') {
|
|
102
|
+
setExecScroll(0);
|
|
103
|
+
}
|
|
104
|
+
else if (input === 'G' && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'exec') {
|
|
105
|
+
const lines = [...(operationResult.stdout || '').split('\n'), ...(operationResult.stderr || '').split('\n')];
|
|
106
|
+
const terminalHeight = stdout?.rows || 30;
|
|
107
|
+
const viewportHeight = Math.max(10, terminalHeight - 15);
|
|
108
|
+
const maxScroll = Math.max(0, lines.length - viewportHeight);
|
|
109
|
+
setExecScroll(maxScroll);
|
|
110
|
+
}
|
|
111
|
+
else if (input === 'c' && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'exec') {
|
|
112
|
+
// Copy exec output to clipboard
|
|
113
|
+
const output = (operationResult.stdout || '') + (operationResult.stderr || '');
|
|
114
|
+
const copyToClipboard = async (text) => {
|
|
115
|
+
const { spawn } = await import('child_process');
|
|
116
|
+
const platform = process.platform;
|
|
117
|
+
let command;
|
|
118
|
+
let args;
|
|
119
|
+
if (platform === 'darwin') {
|
|
120
|
+
command = 'pbcopy';
|
|
121
|
+
args = [];
|
|
122
|
+
}
|
|
123
|
+
else if (platform === 'win32') {
|
|
124
|
+
command = 'clip';
|
|
125
|
+
args = [];
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
command = 'xclip';
|
|
129
|
+
args = ['-selection', 'clipboard'];
|
|
130
|
+
}
|
|
131
|
+
const proc = spawn(command, args);
|
|
132
|
+
proc.stdin.write(text);
|
|
133
|
+
proc.stdin.end();
|
|
134
|
+
proc.on('exit', (code) => {
|
|
135
|
+
if (code === 0) {
|
|
136
|
+
setCopyStatus('Copied to clipboard!');
|
|
137
|
+
setTimeout(() => setCopyStatus(null), 2000);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
setCopyStatus('Failed to copy');
|
|
141
|
+
setTimeout(() => setCopyStatus(null), 2000);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
proc.on('error', () => {
|
|
145
|
+
setCopyStatus('Copy not supported');
|
|
146
|
+
setTimeout(() => setCopyStatus(null), 2000);
|
|
147
|
+
});
|
|
148
|
+
};
|
|
149
|
+
copyToClipboard(output);
|
|
150
|
+
}
|
|
151
|
+
else if ((key.upArrow || input === 'k') && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
|
|
152
|
+
setLogsScroll(Math.max(0, logsScroll - 1));
|
|
153
|
+
}
|
|
154
|
+
else if ((key.downArrow || input === 'j') && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
|
|
155
|
+
setLogsScroll(logsScroll + 1);
|
|
156
|
+
}
|
|
157
|
+
else if (key.pageUp && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
|
|
158
|
+
setLogsScroll(Math.max(0, logsScroll - 10));
|
|
159
|
+
}
|
|
160
|
+
else if (key.pageDown && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
|
|
161
|
+
setLogsScroll(logsScroll + 10);
|
|
162
|
+
}
|
|
163
|
+
else if (input === 'g' && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
|
|
164
|
+
setLogsScroll(0);
|
|
165
|
+
}
|
|
166
|
+
else if (input === 'G' && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
|
|
167
|
+
const logs = operationResult.__logs || [];
|
|
168
|
+
const terminalHeight = stdout?.rows || 30;
|
|
169
|
+
const viewportHeight = Math.max(10, terminalHeight - 10);
|
|
170
|
+
const maxScroll = Math.max(0, logs.length - viewportHeight);
|
|
171
|
+
setLogsScroll(maxScroll);
|
|
172
|
+
}
|
|
173
|
+
else if (input === 'w' && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
|
|
174
|
+
setLogsWrapMode(!logsWrapMode);
|
|
175
|
+
}
|
|
176
|
+
else if (input === 'c' && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
|
|
177
|
+
// Copy logs to clipboard
|
|
178
|
+
const logs = operationResult.__logs || [];
|
|
179
|
+
const logsText = logs.map((log) => {
|
|
180
|
+
const time = new Date(log.timestamp_ms).toLocaleString();
|
|
181
|
+
const level = log.level || 'INFO';
|
|
182
|
+
const source = log.source || 'exec';
|
|
183
|
+
const message = log.message || '';
|
|
184
|
+
const cmd = log.cmd ? `[${log.cmd}] ` : '';
|
|
185
|
+
const exitCode = log.exit_code !== null && log.exit_code !== undefined ? `(${log.exit_code}) ` : '';
|
|
186
|
+
return `${time} ${level}/${source} ${exitCode}${cmd}${message}`;
|
|
187
|
+
}).join('\n');
|
|
188
|
+
const copyToClipboard = async (text) => {
|
|
189
|
+
const { spawn } = await import('child_process');
|
|
190
|
+
const platform = process.platform;
|
|
191
|
+
let command;
|
|
192
|
+
let args;
|
|
193
|
+
if (platform === 'darwin') {
|
|
194
|
+
command = 'pbcopy';
|
|
195
|
+
args = [];
|
|
196
|
+
}
|
|
197
|
+
else if (platform === 'win32') {
|
|
198
|
+
command = 'clip';
|
|
199
|
+
args = [];
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
command = 'xclip';
|
|
203
|
+
args = ['-selection', 'clipboard'];
|
|
204
|
+
}
|
|
205
|
+
const proc = spawn(command, args);
|
|
206
|
+
proc.stdin.write(text);
|
|
207
|
+
proc.stdin.end();
|
|
208
|
+
proc.on('exit', (code) => {
|
|
209
|
+
if (code === 0) {
|
|
210
|
+
setCopyStatus('Copied to clipboard!');
|
|
211
|
+
setTimeout(() => setCopyStatus(null), 2000);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
setCopyStatus('Failed to copy');
|
|
215
|
+
setTimeout(() => setCopyStatus(null), 2000);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
proc.on('error', () => {
|
|
219
|
+
setCopyStatus('Copy not supported');
|
|
220
|
+
setTimeout(() => setCopyStatus(null), 2000);
|
|
221
|
+
});
|
|
222
|
+
};
|
|
223
|
+
copyToClipboard(logsText);
|
|
224
|
+
}
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
// Operations selection mode
|
|
228
|
+
if (input === 'q' || key.escape) {
|
|
229
|
+
console.clear();
|
|
230
|
+
onBack();
|
|
231
|
+
setSelectedOperation(0);
|
|
232
|
+
}
|
|
233
|
+
else if (key.upArrow && selectedOperation > 0) {
|
|
234
|
+
setSelectedOperation(selectedOperation - 1);
|
|
235
|
+
}
|
|
236
|
+
else if (key.downArrow && selectedOperation < operations.length - 1) {
|
|
237
|
+
setSelectedOperation(selectedOperation + 1);
|
|
238
|
+
}
|
|
239
|
+
else if (key.return) {
|
|
240
|
+
console.clear();
|
|
241
|
+
const op = operations[selectedOperation].key;
|
|
242
|
+
setExecutingOperation(op);
|
|
243
|
+
}
|
|
244
|
+
else if (input) {
|
|
245
|
+
// Check if input matches any operation shortcut
|
|
246
|
+
const matchedOp = operations.find(op => op.shortcut === input);
|
|
247
|
+
if (matchedOp) {
|
|
248
|
+
console.clear();
|
|
249
|
+
setExecutingOperation(matchedOp.key);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
const executeOperation = async () => {
|
|
254
|
+
const client = getClient();
|
|
255
|
+
try {
|
|
256
|
+
setLoading(true);
|
|
257
|
+
switch (executingOperation) {
|
|
258
|
+
case 'exec':
|
|
259
|
+
const execResult = await client.devboxes.executeSync(devbox.id, {
|
|
260
|
+
command: operationInput,
|
|
261
|
+
});
|
|
262
|
+
// Format exec result for custom rendering
|
|
263
|
+
const formattedExecResult = {
|
|
264
|
+
__customRender: 'exec',
|
|
265
|
+
command: operationInput,
|
|
266
|
+
stdout: execResult.stdout || '',
|
|
267
|
+
stderr: execResult.stderr || '',
|
|
268
|
+
exitCode: execResult.exit_code ?? 0,
|
|
269
|
+
};
|
|
270
|
+
setOperationResult(formattedExecResult);
|
|
271
|
+
break;
|
|
272
|
+
case 'upload':
|
|
273
|
+
const fs = await import('fs');
|
|
274
|
+
const fileStream = fs.createReadStream(operationInput);
|
|
275
|
+
const filename = operationInput.split('/').pop() || 'file';
|
|
276
|
+
await client.devboxes.uploadFile(devbox.id, {
|
|
277
|
+
path: filename,
|
|
278
|
+
file: fileStream,
|
|
279
|
+
});
|
|
280
|
+
setOperationResult(`File ${filename} uploaded successfully`);
|
|
281
|
+
break;
|
|
282
|
+
case 'snapshot':
|
|
283
|
+
const snapshot = await client.devboxes.snapshotDisk(devbox.id, {
|
|
284
|
+
name: operationInput || `snapshot-${Date.now()}`,
|
|
285
|
+
});
|
|
286
|
+
setOperationResult(`Snapshot created: ${snapshot.id}`);
|
|
287
|
+
break;
|
|
288
|
+
case 'ssh':
|
|
289
|
+
const sshKey = await client.devboxes.createSSHKey(devbox.id);
|
|
290
|
+
const fsModule = await import('fs');
|
|
291
|
+
const pathModule = await import('path');
|
|
292
|
+
const osModule = await import('os');
|
|
293
|
+
const sshDir = pathModule.join(osModule.homedir(), '.runloop', 'ssh_keys');
|
|
294
|
+
fsModule.mkdirSync(sshDir, { recursive: true });
|
|
295
|
+
const keyPath = pathModule.join(sshDir, `${devbox.id}.pem`);
|
|
296
|
+
fsModule.writeFileSync(keyPath, sshKey.ssh_private_key, { mode: 0o600 });
|
|
297
|
+
const sshUser = devbox.launch_parameters?.user_parameters?.username || 'user';
|
|
298
|
+
const proxyCommand = 'openssl s_client -quiet -verify_quiet -servername %h -connect ssh.runloop.ai:443 2>/dev/null';
|
|
299
|
+
global.__sshCommand = {
|
|
300
|
+
keyPath,
|
|
301
|
+
proxyCommand,
|
|
302
|
+
sshUser,
|
|
303
|
+
url: sshKey.url,
|
|
304
|
+
devboxName: devbox.name || devbox.id
|
|
305
|
+
};
|
|
306
|
+
exit();
|
|
307
|
+
break;
|
|
308
|
+
case 'logs':
|
|
309
|
+
const logsResult = await client.devboxes.logs.list(devbox.id);
|
|
310
|
+
if (logsResult.logs.length === 0) {
|
|
311
|
+
setOperationResult('No logs available for this devbox.');
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
logsResult.__customRender = 'logs';
|
|
315
|
+
logsResult.__logs = logsResult.logs;
|
|
316
|
+
logsResult.__totalCount = logsResult.logs.length;
|
|
317
|
+
setOperationResult(logsResult);
|
|
318
|
+
}
|
|
319
|
+
break;
|
|
320
|
+
case 'tunnel':
|
|
321
|
+
const port = parseInt(operationInput);
|
|
322
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
323
|
+
setOperationError(new Error('Invalid port number. Please enter a port between 1 and 65535.'));
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
const tunnel = await client.devboxes.createTunnel(devbox.id, { port });
|
|
327
|
+
setOperationResult(`Tunnel created!\n\n` +
|
|
328
|
+
`Local Port: ${port}\n` +
|
|
329
|
+
`Public URL: ${tunnel.url}\n\n` +
|
|
330
|
+
`You can now access port ${port} on the devbox via:\n${tunnel.url}`);
|
|
331
|
+
}
|
|
332
|
+
break;
|
|
333
|
+
case 'suspend':
|
|
334
|
+
await client.devboxes.suspend(devbox.id);
|
|
335
|
+
setOperationResult(`Devbox ${devbox.id} suspended successfully`);
|
|
336
|
+
break;
|
|
337
|
+
case 'resume':
|
|
338
|
+
await client.devboxes.resume(devbox.id);
|
|
339
|
+
setOperationResult(`Devbox ${devbox.id} resumed successfully`);
|
|
340
|
+
break;
|
|
341
|
+
case 'delete':
|
|
342
|
+
await client.devboxes.shutdown(devbox.id);
|
|
343
|
+
setOperationResult(`Devbox ${devbox.id} shut down successfully`);
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
catch (err) {
|
|
348
|
+
setOperationError(err);
|
|
349
|
+
}
|
|
350
|
+
finally {
|
|
351
|
+
setLoading(false);
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
const operationLabel = operations.find((o) => o.key === executingOperation)?.label || 'Operation';
|
|
355
|
+
// Operation result display
|
|
356
|
+
if (operationResult || operationError) {
|
|
357
|
+
// Check for custom exec rendering
|
|
358
|
+
if (operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'exec') {
|
|
359
|
+
const command = operationResult.command || '';
|
|
360
|
+
const stdout = operationResult.stdout || '';
|
|
361
|
+
const stderr = operationResult.stderr || '';
|
|
362
|
+
const exitCode = operationResult.exitCode;
|
|
363
|
+
const stdoutLines = stdout ? stdout.split('\n') : [];
|
|
364
|
+
const stderrLines = stderr ? stderr.split('\n') : [];
|
|
365
|
+
const allLines = [...stdoutLines, ...stderrLines].filter(line => line !== '');
|
|
366
|
+
const terminalHeight = stdout?.rows || 30;
|
|
367
|
+
const viewportHeight = Math.max(10, terminalHeight - 15);
|
|
368
|
+
const maxScroll = Math.max(0, allLines.length - viewportHeight);
|
|
369
|
+
const actualScroll = Math.min(execScroll, maxScroll);
|
|
370
|
+
const visibleLines = allLines.slice(actualScroll, actualScroll + viewportHeight);
|
|
371
|
+
const hasMore = actualScroll + viewportHeight < allLines.length;
|
|
372
|
+
const hasLess = actualScroll > 0;
|
|
373
|
+
const exitCodeColor = exitCode === 0 ? 'green' : 'red';
|
|
374
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [...breadcrumbItems, { label: 'Execute Command', active: true }] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, marginBottom: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: "cyan", bold: true, children: [figures.play, " Command:"] }), _jsx(Text, { children: " " }), _jsx(Text, { color: "white", children: command })] }), _jsxs(Box, { children: [_jsx(Text, { color: "gray", dimColor: true, children: "Exit Code: " }), _jsx(Text, { color: exitCodeColor, bold: true, children: exitCode })] })] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [allLines.length === 0 && (_jsx(Text, { color: "gray", dimColor: true, children: "No output" })), visibleLines.map((line, index) => {
|
|
375
|
+
const actualIndex = actualScroll + index;
|
|
376
|
+
const isStderr = actualIndex >= stdoutLines.length;
|
|
377
|
+
const lineColor = isStderr ? 'red' : 'white';
|
|
378
|
+
return (_jsx(Box, { children: _jsx(Text, { color: lineColor, children: line }) }, index));
|
|
379
|
+
}), hasLess && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "cyan", children: [figures.arrowUp, " More above"] }) })), hasMore && (_jsx(Box, { marginTop: hasLess ? 0 : 1, children: _jsxs(Text, { color: "cyan", children: [figures.arrowDown, " More below"] }) }))] }), _jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: "cyan", bold: true, children: [figures.hamburger, " ", allLines.length] }), _jsx(Text, { color: "gray", dimColor: true, children: " lines" }), allLines.length > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", dimColor: true, children: " \u2022 " }), _jsxs(Text, { color: "gray", dimColor: true, children: ["Viewing ", actualScroll + 1, "-", Math.min(actualScroll + viewportHeight, allLines.length), " of ", allLines.length] })] })), stdout && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", dimColor: true, children: " \u2022 " }), _jsxs(Text, { color: "green", dimColor: true, children: ["stdout: ", stdoutLines.length, " lines"] })] })), stderr && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", dimColor: true, children: " \u2022 " }), _jsxs(Text, { color: "red", dimColor: true, children: ["stderr: ", stderrLines.length, " lines"] })] })), copyStatus && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", dimColor: true, children: " \u2022 " }), _jsx(Text, { color: "green", bold: true, children: copyStatus })] }))] }), _jsx(Box, { marginTop: 1, paddingX: 1, children: _jsxs(Text, { color: "gray", dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [g] Top \u2022 [G] Bottom \u2022 [c] Copy \u2022 [Enter], [q], or [esc] Back"] }) })] }));
|
|
380
|
+
}
|
|
381
|
+
// Check for custom logs rendering
|
|
382
|
+
if (operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
|
|
383
|
+
const logs = operationResult.__logs || [];
|
|
384
|
+
const totalCount = operationResult.__totalCount || 0;
|
|
385
|
+
const terminalHeight = stdout?.rows || 30;
|
|
386
|
+
const terminalWidth = stdout?.columns || 120;
|
|
387
|
+
const viewportHeight = Math.max(10, terminalHeight - 10);
|
|
388
|
+
const maxScroll = Math.max(0, logs.length - viewportHeight);
|
|
389
|
+
const actualScroll = Math.min(logsScroll, maxScroll);
|
|
390
|
+
const visibleLogs = logs.slice(actualScroll, actualScroll + viewportHeight);
|
|
391
|
+
const hasMore = actualScroll + viewportHeight < logs.length;
|
|
392
|
+
const hasLess = actualScroll > 0;
|
|
393
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [...breadcrumbItems, { label: 'Logs', active: true }] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [visibleLogs.map((log, index) => {
|
|
394
|
+
const time = new Date(log.timestamp_ms).toLocaleTimeString();
|
|
395
|
+
const level = log.level ? log.level[0].toUpperCase() : 'I';
|
|
396
|
+
const source = log.source ? log.source.substring(0, 8) : 'exec';
|
|
397
|
+
const fullMessage = log.message || '';
|
|
398
|
+
const cmd = log.cmd ? `[${log.cmd.substring(0, 40)}${log.cmd.length > 40 ? '...' : ''}] ` : '';
|
|
399
|
+
const exitCode = log.exit_code !== null && log.exit_code !== undefined ? `(${log.exit_code}) ` : '';
|
|
400
|
+
let levelColor = 'gray';
|
|
401
|
+
if (level === 'E')
|
|
402
|
+
levelColor = 'red';
|
|
403
|
+
else if (level === 'W')
|
|
404
|
+
levelColor = 'yellow';
|
|
405
|
+
else if (level === 'I')
|
|
406
|
+
levelColor = 'cyan';
|
|
407
|
+
if (logsWrapMode) {
|
|
408
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: "gray", dimColor: true, children: time }), _jsx(Text, { children: " " }), _jsx(Text, { color: levelColor, bold: true, children: level }), _jsxs(Text, { color: "gray", dimColor: true, children: ["/", source] }), _jsx(Text, { children: " " }), exitCode && _jsx(Text, { color: "yellow", children: exitCode }), cmd && _jsx(Text, { color: "blue", dimColor: true, children: cmd }), _jsx(Text, { children: fullMessage })] }, index));
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
const metadataWidth = 11 + 1 + 1 + 1 + 8 + 1 + exitCode.length + cmd.length + 6;
|
|
412
|
+
const availableMessageWidth = Math.max(20, terminalWidth - metadataWidth);
|
|
413
|
+
const truncatedMessage = fullMessage.length > availableMessageWidth
|
|
414
|
+
? fullMessage.substring(0, availableMessageWidth - 3) + '...'
|
|
415
|
+
: fullMessage;
|
|
416
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: "gray", dimColor: true, children: time }), _jsx(Text, { children: " " }), _jsx(Text, { color: levelColor, bold: true, children: level }), _jsxs(Text, { color: "gray", dimColor: true, children: ["/", source] }), _jsx(Text, { children: " " }), exitCode && _jsx(Text, { color: "yellow", children: exitCode }), cmd && _jsx(Text, { color: "blue", dimColor: true, children: cmd }), _jsx(Text, { children: truncatedMessage })] }, index));
|
|
417
|
+
}
|
|
418
|
+
}), hasLess && (_jsx(Box, { children: _jsxs(Text, { color: "cyan", children: [figures.arrowUp, " More above"] }) })), hasMore && (_jsx(Box, { children: _jsxs(Text, { color: "cyan", children: [figures.arrowDown, " More below"] }) }))] }), _jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: "cyan", bold: true, children: [figures.hamburger, " ", totalCount] }), _jsx(Text, { color: "gray", dimColor: true, children: " total logs" }), _jsx(Text, { color: "gray", dimColor: true, children: " \u2022 " }), _jsxs(Text, { color: "gray", dimColor: true, children: ["Viewing ", actualScroll + 1, "-", Math.min(actualScroll + viewportHeight, logs.length), " of ", logs.length] }), _jsx(Text, { color: "gray", dimColor: true, children: " \u2022 " }), _jsx(Text, { color: logsWrapMode ? 'green' : 'gray', bold: logsWrapMode, children: logsWrapMode ? 'Wrap: ON' : 'Wrap: OFF' }), copyStatus && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", dimColor: true, children: " \u2022 " }), _jsx(Text, { color: "green", bold: true, children: copyStatus })] }))] }), _jsx(Box, { marginTop: 1, paddingX: 1, children: _jsxs(Text, { color: "gray", dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [g] Top \u2022 [G] Bottom \u2022 [w] Toggle Wrap \u2022 [c] Copy \u2022 [Enter], [q], or [esc] Back"] }) })] }));
|
|
419
|
+
}
|
|
420
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [...breadcrumbItems, { label: operationLabel, active: true }] }), _jsx(Header, { title: "Operation Result" }), operationResult && _jsx(SuccessMessage, { message: operationResult }), operationError && _jsx(ErrorMessage, { message: "Operation failed", error: operationError }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "Press [Enter], [q], or [esc] to continue" }) })] }));
|
|
421
|
+
}
|
|
422
|
+
// Operation input mode
|
|
423
|
+
if (executingOperation && devbox) {
|
|
424
|
+
const needsInput = executingOperation === 'exec' ||
|
|
425
|
+
executingOperation === 'upload' ||
|
|
426
|
+
executingOperation === 'snapshot' ||
|
|
427
|
+
executingOperation === 'tunnel';
|
|
428
|
+
if (loading) {
|
|
429
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [...breadcrumbItems, { label: operationLabel, active: true }] }), _jsx(Header, { title: "Executing Operation" }), _jsx(SpinnerComponent, { message: "Please wait..." })] }));
|
|
430
|
+
}
|
|
431
|
+
if (!needsInput) {
|
|
432
|
+
const messages = {
|
|
433
|
+
ssh: 'Creating SSH key...',
|
|
434
|
+
logs: 'Fetching logs...',
|
|
435
|
+
suspend: 'Suspending devbox...',
|
|
436
|
+
resume: 'Resuming devbox...',
|
|
437
|
+
delete: 'Shutting down devbox...',
|
|
438
|
+
};
|
|
439
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [...breadcrumbItems, { label: operationLabel, active: true }] }), _jsx(Header, { title: "Executing Operation" }), _jsx(SpinnerComponent, { message: messages[executingOperation] || 'Please wait...' })] }));
|
|
440
|
+
}
|
|
441
|
+
const prompts = {
|
|
442
|
+
exec: 'Command to execute:',
|
|
443
|
+
upload: 'File path to upload:',
|
|
444
|
+
snapshot: 'Snapshot name (optional):',
|
|
445
|
+
tunnel: 'Port number to expose:',
|
|
446
|
+
};
|
|
447
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [...breadcrumbItems, { label: operationLabel, active: true }] }), _jsx(Header, { title: operationLabel }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "cyan", bold: true, children: devbox.name || devbox.id }) }), _jsx(Box, { children: _jsxs(Text, { color: "gray", children: [prompts[executingOperation], " "] }) }), _jsx(Box, { marginTop: 1, children: _jsx(TextInput, { value: operationInput, onChange: setOperationInput, placeholder: executingOperation === 'exec'
|
|
448
|
+
? 'ls -la'
|
|
449
|
+
: executingOperation === 'upload'
|
|
450
|
+
? '/path/to/file'
|
|
451
|
+
: executingOperation === 'tunnel'
|
|
452
|
+
? '8080'
|
|
453
|
+
: 'my-snapshot' }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "Press [Enter] to execute \u2022 [q or esc] Cancel" }) })] })] }));
|
|
454
|
+
}
|
|
455
|
+
// Operations selection mode
|
|
456
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "cyan", bold: true, children: [figures.play, " Operations"] }), _jsx(Box, { flexDirection: "column", children: operations.map((op, index) => {
|
|
457
|
+
const isSelected = index === selectedOperation;
|
|
458
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { color: isSelected ? 'cyan' : 'gray', children: [isSelected ? figures.pointer : ' ', " "] }), _jsxs(Text, { color: isSelected ? op.color : 'gray', bold: isSelected, children: [op.icon, " ", op.label] }), _jsxs(Text, { color: "gray", dimColor: true, children: [" [", op.shortcut, "]"] })] }, op.key));
|
|
459
|
+
}) })] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "gray", dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [Enter] Select \u2022 [q] Back"] }) })] }));
|
|
460
|
+
};
|
|
@@ -4,14 +4,13 @@ import { Box, Text, useInput } from 'ink';
|
|
|
4
4
|
import TextInput from 'ink-text-input';
|
|
5
5
|
import figures from 'figures';
|
|
6
6
|
import { getClient } from '../utils/client.js';
|
|
7
|
-
import { Header } from './Header.js';
|
|
8
7
|
import { SpinnerComponent } from './Spinner.js';
|
|
9
8
|
import { ErrorMessage } from './ErrorMessage.js';
|
|
10
9
|
import { SuccessMessage } from './SuccessMessage.js';
|
|
11
10
|
import { Breadcrumb } from './Breadcrumb.js';
|
|
12
11
|
import { MetadataDisplay } from './MetadataDisplay.js';
|
|
13
12
|
export const DevboxCreatePage = ({ onBack, onCreate }) => {
|
|
14
|
-
const [currentField, setCurrentField] = React.useState('
|
|
13
|
+
const [currentField, setCurrentField] = React.useState('create');
|
|
15
14
|
const [formData, setFormData] = React.useState({
|
|
16
15
|
name: '',
|
|
17
16
|
architecture: 'arm64',
|
|
@@ -33,6 +32,7 @@ export const DevboxCreatePage = ({ onBack, onCreate }) => {
|
|
|
33
32
|
const [result, setResult] = React.useState(null);
|
|
34
33
|
const [error, setError] = React.useState(null);
|
|
35
34
|
const baseFields = [
|
|
35
|
+
{ key: 'create', label: 'Devbox Create', type: 'action' },
|
|
36
36
|
{ key: 'name', label: 'Name', type: 'text' },
|
|
37
37
|
{ key: 'architecture', label: 'Architecture', type: 'select' },
|
|
38
38
|
{ key: 'resource_size', label: 'Resource Size', type: 'select' },
|
|
@@ -93,6 +93,11 @@ export const DevboxCreatePage = ({ onBack, onCreate }) => {
|
|
|
93
93
|
handleCreate();
|
|
94
94
|
return;
|
|
95
95
|
}
|
|
96
|
+
// Handle Enter on create field
|
|
97
|
+
if (currentField === 'create' && key.return) {
|
|
98
|
+
handleCreate();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
96
101
|
// Handle metadata section
|
|
97
102
|
if (inMetadataSection) {
|
|
98
103
|
const metadataKeys = Object.keys(formData.metadata);
|
|
@@ -312,29 +317,32 @@ export const DevboxCreatePage = ({ onBack, onCreate }) => {
|
|
|
312
317
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
313
318
|
{ label: 'Devboxes' },
|
|
314
319
|
{ label: 'Create', active: true }
|
|
315
|
-
] }), _jsx(
|
|
320
|
+
] }), _jsx(SuccessMessage, { message: "Devbox created successfully!", details: `ID: ${result.id}\nName: ${result.name || '(none)'}\nStatus: ${result.status}` }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "Press [Enter], [q], or [esc] to return to list" }) })] }));
|
|
316
321
|
}
|
|
317
322
|
// Error screen
|
|
318
323
|
if (error) {
|
|
319
324
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
320
325
|
{ label: 'Devboxes' },
|
|
321
326
|
{ label: 'Create', active: true }
|
|
322
|
-
] }), _jsx(
|
|
327
|
+
] }), _jsx(ErrorMessage, { message: "Failed to create devbox", error: error }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "Press [Enter] or [r] to retry \u2022 [q] or [esc] to cancel" }) })] }));
|
|
323
328
|
}
|
|
324
329
|
// Creating screen
|
|
325
330
|
if (creating) {
|
|
326
331
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
327
332
|
{ label: 'Devboxes' },
|
|
328
333
|
{ label: 'Create', active: true }
|
|
329
|
-
] }), _jsx(
|
|
334
|
+
] }), _jsx(SpinnerComponent, { message: "Creating devbox..." })] }));
|
|
330
335
|
}
|
|
331
336
|
// Form screen
|
|
332
337
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
333
338
|
{ label: 'Devboxes' },
|
|
334
339
|
{ label: 'Create', active: true }
|
|
335
|
-
] }), _jsx(
|
|
340
|
+
] }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: fields.map((field, index) => {
|
|
336
341
|
const isActive = currentField === field.key;
|
|
337
342
|
const fieldData = formData[field.key];
|
|
343
|
+
if (field.type === 'action') {
|
|
344
|
+
return (_jsxs(Box, { marginBottom: 0, children: [_jsxs(Text, { color: isActive ? 'green' : 'gray', bold: isActive, children: [isActive ? figures.pointer : ' ', " ", field.label] }), isActive && (_jsxs(Text, { color: "gray", dimColor: true, children: [' ', "[Enter to create]"] }))] }, field.key));
|
|
345
|
+
}
|
|
338
346
|
if (field.type === 'text') {
|
|
339
347
|
return (_jsxs(Box, { marginBottom: 0, children: [_jsxs(Text, { color: isActive ? 'cyan' : 'gray', children: [isActive ? figures.pointer : ' ', " ", field.label, ":", ' '] }), isActive ? (_jsx(TextInput, { value: String(fieldData || ''), onChange: (value) => {
|
|
340
348
|
setFormData({ ...formData, [field.key]: value });
|
|
@@ -366,5 +374,5 @@ export const DevboxCreatePage = ({ onBack, onCreate }) => {
|
|
|
366
374
|
: `${figures.arrowUp}${figures.arrowDown} Navigate • [Enter] ${selectedMetadataIndex === 0 ? 'Add' : selectedMetadataIndex === maxIndex ? 'Done' : 'Edit'} • [d] Delete • [esc] Back` }) })] }, field.key));
|
|
367
375
|
}
|
|
368
376
|
return null;
|
|
369
|
-
}) }), formData.resource_size === 'CUSTOM_SIZE' && validateCustomResources() && (_jsxs(Box, { borderStyle: "round", borderColor: "red", paddingX: 1, paddingY: 0, marginTop: 1, children: [_jsxs(Text, { color: "red", bold: true, children: [figures.cross, " Validation Error"] }), _jsx(Text, { color: "red", dimColor: true, children: validateCustomResources() })] })),
|
|
377
|
+
}) }), formData.resource_size === 'CUSTOM_SIZE' && validateCustomResources() && (_jsxs(Box, { borderStyle: "round", borderColor: "red", paddingX: 1, paddingY: 0, marginTop: 1, children: [_jsxs(Text, { color: "red", bold: true, children: [figures.cross, " Validation Error"] }), _jsx(Text, { color: "red", dimColor: true, children: validateCustomResources() })] })), !inMetadataSection && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "gray", dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [Enter] Create \u2022 [q] Cancel"] }) }))] }));
|
|
370
378
|
};
|