@runloop/rl-cli 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 +21 -0
- package/README.md +190 -0
- package/dist/cli.js +110 -0
- package/dist/commands/auth.js +27 -0
- package/dist/commands/blueprint/list.js +373 -0
- package/dist/commands/create.js +42 -0
- package/dist/commands/delete.js +34 -0
- package/dist/commands/devbox/create.js +45 -0
- package/dist/commands/devbox/delete.js +37 -0
- package/dist/commands/devbox/exec.js +35 -0
- package/dist/commands/devbox/list.js +302 -0
- package/dist/commands/devbox/upload.js +40 -0
- package/dist/commands/exec.js +35 -0
- package/dist/commands/list.js +59 -0
- package/dist/commands/snapshot/create.js +40 -0
- package/dist/commands/snapshot/delete.js +37 -0
- package/dist/commands/snapshot/list.js +122 -0
- package/dist/commands/upload.js +40 -0
- package/dist/components/Banner.js +11 -0
- package/dist/components/Breadcrumb.js +9 -0
- package/dist/components/DetailView.js +29 -0
- package/dist/components/DevboxCard.js +24 -0
- package/dist/components/DevboxCreatePage.js +370 -0
- package/dist/components/DevboxDetailPage.js +621 -0
- package/dist/components/ErrorMessage.js +6 -0
- package/dist/components/Header.js +6 -0
- package/dist/components/MetadataDisplay.js +24 -0
- package/dist/components/OperationsMenu.js +19 -0
- package/dist/components/Spinner.js +6 -0
- package/dist/components/StatusBadge.js +41 -0
- package/dist/components/SuccessMessage.js +6 -0
- package/dist/components/Table.example.js +55 -0
- package/dist/components/Table.js +54 -0
- package/dist/utils/CommandExecutor.js +115 -0
- package/dist/utils/client.js +13 -0
- package/dist/utils/config.js +17 -0
- package/dist/utils/output.js +115 -0
- package/package.json +69 -0
|
@@ -0,0 +1,621 @@
|
|
|
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 { StatusBadge } from './StatusBadge.js';
|
|
12
|
+
import { MetadataDisplay } from './MetadataDisplay.js';
|
|
13
|
+
import { Breadcrumb } from './Breadcrumb.js';
|
|
14
|
+
// Format time ago in a succinct way
|
|
15
|
+
const formatTimeAgo = (timestamp) => {
|
|
16
|
+
const seconds = Math.floor((Date.now() - timestamp) / 1000);
|
|
17
|
+
if (seconds < 60)
|
|
18
|
+
return `${seconds}s ago`;
|
|
19
|
+
const minutes = Math.floor(seconds / 60);
|
|
20
|
+
if (minutes < 60)
|
|
21
|
+
return `${minutes}m ago`;
|
|
22
|
+
const hours = Math.floor(minutes / 60);
|
|
23
|
+
if (hours < 24)
|
|
24
|
+
return `${hours}h ago`;
|
|
25
|
+
const days = Math.floor(hours / 24);
|
|
26
|
+
if (days < 30)
|
|
27
|
+
return `${days}d ago`;
|
|
28
|
+
const months = Math.floor(days / 30);
|
|
29
|
+
if (months < 12)
|
|
30
|
+
return `${months}mo ago`;
|
|
31
|
+
const years = Math.floor(months / 12);
|
|
32
|
+
return `${years}y ago`;
|
|
33
|
+
};
|
|
34
|
+
export const DevboxDetailPage = ({ devbox: initialDevbox, onBack }) => {
|
|
35
|
+
const { exit } = useApp();
|
|
36
|
+
const { stdout } = useStdout();
|
|
37
|
+
const [loading, setLoading] = React.useState(false);
|
|
38
|
+
const [selectedOperation, setSelectedOperation] = React.useState(0);
|
|
39
|
+
const [executingOperation, setExecutingOperation] = React.useState(null);
|
|
40
|
+
const [operationInput, setOperationInput] = React.useState('');
|
|
41
|
+
const [operationResult, setOperationResult] = React.useState(null);
|
|
42
|
+
const [operationError, setOperationError] = React.useState(null);
|
|
43
|
+
const [showDetailedInfo, setShowDetailedInfo] = React.useState(false);
|
|
44
|
+
const [detailScroll, setDetailScroll] = React.useState(0);
|
|
45
|
+
const [logsWrapMode, setLogsWrapMode] = React.useState(true);
|
|
46
|
+
const [logsScroll, setLogsScroll] = React.useState(0);
|
|
47
|
+
const [copyStatus, setCopyStatus] = React.useState(null);
|
|
48
|
+
const selectedDevbox = initialDevbox;
|
|
49
|
+
// Memoize time-based values to prevent re-rendering on every tick
|
|
50
|
+
const formattedCreateTime = React.useMemo(() => selectedDevbox.create_time_ms ? new Date(selectedDevbox.create_time_ms).toLocaleString() : '', [selectedDevbox.create_time_ms]);
|
|
51
|
+
const createTimeAgo = React.useMemo(() => selectedDevbox.create_time_ms ? formatTimeAgo(selectedDevbox.create_time_ms) : '', [selectedDevbox.create_time_ms]);
|
|
52
|
+
const allOperations = [
|
|
53
|
+
{ key: 'logs', label: 'View Logs', color: 'blue', icon: figures.info },
|
|
54
|
+
{ key: 'exec', label: 'Execute Command', color: 'green', icon: figures.play },
|
|
55
|
+
{ key: 'upload', label: 'Upload File', color: 'green', icon: figures.arrowUp },
|
|
56
|
+
{ key: 'snapshot', label: 'Create Snapshot', color: 'yellow', icon: figures.circleFilled },
|
|
57
|
+
{ key: 'ssh', label: 'SSH onto the box', color: 'cyan', icon: figures.arrowRight },
|
|
58
|
+
{ key: 'tunnel', label: 'Open Tunnel', color: 'magenta', icon: figures.pointerSmall },
|
|
59
|
+
{ key: 'suspend', label: 'Suspend Devbox', color: 'yellow', icon: figures.squareSmallFilled },
|
|
60
|
+
{ key: 'resume', label: 'Resume Devbox', color: 'green', icon: figures.play },
|
|
61
|
+
{ key: 'delete', label: 'Shutdown Devbox', color: 'red', icon: figures.cross },
|
|
62
|
+
];
|
|
63
|
+
// Filter operations based on devbox status
|
|
64
|
+
const operations = selectedDevbox ? allOperations.filter(op => {
|
|
65
|
+
const status = selectedDevbox.status;
|
|
66
|
+
// When suspended: logs and resume
|
|
67
|
+
if (status === 'suspended') {
|
|
68
|
+
return op.key === 'resume' || op.key === 'logs';
|
|
69
|
+
}
|
|
70
|
+
// When not running (shutdown, failure, etc): only logs
|
|
71
|
+
if (status !== 'running' && status !== 'provisioning' && status !== 'initializing') {
|
|
72
|
+
return op.key === 'logs';
|
|
73
|
+
}
|
|
74
|
+
// When running: everything except resume
|
|
75
|
+
if (status === 'running') {
|
|
76
|
+
return op.key !== 'resume';
|
|
77
|
+
}
|
|
78
|
+
// Default for transitional states (provisioning, initializing)
|
|
79
|
+
return op.key === 'logs' || op.key === 'delete';
|
|
80
|
+
}) : allOperations;
|
|
81
|
+
// Auto-execute operations that don't need input (delete, ssh, logs, suspend, resume)
|
|
82
|
+
React.useEffect(() => {
|
|
83
|
+
if ((executingOperation === 'delete' || executingOperation === 'ssh' || executingOperation === 'logs' || executingOperation === 'suspend' || executingOperation === 'resume') && !loading && selectedDevbox) {
|
|
84
|
+
executeOperation();
|
|
85
|
+
}
|
|
86
|
+
}, [executingOperation]);
|
|
87
|
+
useInput((input, key) => {
|
|
88
|
+
// Handle operation input mode
|
|
89
|
+
if (executingOperation && !operationResult && !operationError) {
|
|
90
|
+
if (key.return && operationInput.trim()) {
|
|
91
|
+
executeOperation();
|
|
92
|
+
}
|
|
93
|
+
else if (input === 'q' || key.escape) {
|
|
94
|
+
console.clear();
|
|
95
|
+
setExecutingOperation(null);
|
|
96
|
+
setOperationInput('');
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// Handle operation result display
|
|
101
|
+
if (operationResult || operationError) {
|
|
102
|
+
if (input === 'q' || key.escape || key.return) {
|
|
103
|
+
console.clear();
|
|
104
|
+
setOperationResult(null);
|
|
105
|
+
setOperationError(null);
|
|
106
|
+
setExecutingOperation(null);
|
|
107
|
+
setOperationInput('');
|
|
108
|
+
setLogsWrapMode(true); // Reset wrap mode
|
|
109
|
+
setLogsScroll(0); // Reset scroll
|
|
110
|
+
setCopyStatus(null); // Reset copy status
|
|
111
|
+
// Keep detail view open
|
|
112
|
+
}
|
|
113
|
+
else if ((key.upArrow || input === 'k') && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
|
|
114
|
+
// Scroll up in logs
|
|
115
|
+
setLogsScroll(Math.max(0, logsScroll - 1));
|
|
116
|
+
}
|
|
117
|
+
else if ((key.downArrow || input === 'j') && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
|
|
118
|
+
// Scroll down in logs
|
|
119
|
+
setLogsScroll(logsScroll + 1);
|
|
120
|
+
}
|
|
121
|
+
else if (key.pageUp && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
|
|
122
|
+
// Page up
|
|
123
|
+
setLogsScroll(Math.max(0, logsScroll - 10));
|
|
124
|
+
}
|
|
125
|
+
else if (key.pageDown && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
|
|
126
|
+
// Page down
|
|
127
|
+
setLogsScroll(logsScroll + 10);
|
|
128
|
+
}
|
|
129
|
+
else if (input === 'g' && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
|
|
130
|
+
// Jump to top
|
|
131
|
+
setLogsScroll(0);
|
|
132
|
+
}
|
|
133
|
+
else if (input === 'G' && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
|
|
134
|
+
// Jump to bottom (last line)
|
|
135
|
+
const logs = operationResult.__logs || [];
|
|
136
|
+
const terminalHeight = stdout?.rows || 30;
|
|
137
|
+
const viewportHeight = Math.max(10, terminalHeight - 10);
|
|
138
|
+
const maxScroll = Math.max(0, logs.length - viewportHeight);
|
|
139
|
+
setLogsScroll(maxScroll);
|
|
140
|
+
}
|
|
141
|
+
else if (input === 'w' && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
|
|
142
|
+
// Toggle wrap mode for logs
|
|
143
|
+
setLogsWrapMode(!logsWrapMode);
|
|
144
|
+
}
|
|
145
|
+
else if (input === 'c' && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
|
|
146
|
+
// Copy logs to clipboard
|
|
147
|
+
const logs = operationResult.__logs || [];
|
|
148
|
+
const logsText = logs.map((log) => {
|
|
149
|
+
const time = new Date(log.timestamp_ms).toLocaleString();
|
|
150
|
+
const level = log.level || 'INFO';
|
|
151
|
+
const source = log.source || 'exec';
|
|
152
|
+
const message = log.message || '';
|
|
153
|
+
const cmd = log.cmd ? `[${log.cmd}] ` : '';
|
|
154
|
+
const exitCode = log.exit_code !== null && log.exit_code !== undefined ? `(${log.exit_code}) ` : '';
|
|
155
|
+
return `${time} ${level}/${source} ${exitCode}${cmd}${message}`;
|
|
156
|
+
}).join('\n');
|
|
157
|
+
// Copy to clipboard using pbcopy (macOS), xclip (Linux), or clip (Windows)
|
|
158
|
+
const copyToClipboard = async (text) => {
|
|
159
|
+
const { spawn } = await import('child_process');
|
|
160
|
+
const platform = process.platform;
|
|
161
|
+
let command;
|
|
162
|
+
let args;
|
|
163
|
+
if (platform === 'darwin') {
|
|
164
|
+
command = 'pbcopy';
|
|
165
|
+
args = [];
|
|
166
|
+
}
|
|
167
|
+
else if (platform === 'win32') {
|
|
168
|
+
command = 'clip';
|
|
169
|
+
args = [];
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
command = 'xclip';
|
|
173
|
+
args = ['-selection', 'clipboard'];
|
|
174
|
+
}
|
|
175
|
+
const proc = spawn(command, args);
|
|
176
|
+
proc.stdin.write(text);
|
|
177
|
+
proc.stdin.end();
|
|
178
|
+
proc.on('exit', (code) => {
|
|
179
|
+
if (code === 0) {
|
|
180
|
+
setCopyStatus('Copied to clipboard!');
|
|
181
|
+
setTimeout(() => setCopyStatus(null), 2000);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
setCopyStatus('Failed to copy');
|
|
185
|
+
setTimeout(() => setCopyStatus(null), 2000);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
proc.on('error', () => {
|
|
189
|
+
setCopyStatus('Copy not supported');
|
|
190
|
+
setTimeout(() => setCopyStatus(null), 2000);
|
|
191
|
+
});
|
|
192
|
+
};
|
|
193
|
+
copyToClipboard(logsText);
|
|
194
|
+
}
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
// Handle detailed info mode
|
|
198
|
+
if (showDetailedInfo) {
|
|
199
|
+
if (input === 'q' || key.escape) {
|
|
200
|
+
setShowDetailedInfo(false);
|
|
201
|
+
setDetailScroll(0);
|
|
202
|
+
}
|
|
203
|
+
else if (input === 'j' || input === 's' || key.downArrow) {
|
|
204
|
+
// Scroll down in detailed info
|
|
205
|
+
setDetailScroll(detailScroll + 1);
|
|
206
|
+
}
|
|
207
|
+
else if (input === 'k' || input === 'w' || key.upArrow) {
|
|
208
|
+
// Scroll up in detailed info
|
|
209
|
+
setDetailScroll(Math.max(0, detailScroll - 1));
|
|
210
|
+
}
|
|
211
|
+
else if (key.pageDown) {
|
|
212
|
+
// Page down
|
|
213
|
+
setDetailScroll(detailScroll + 10);
|
|
214
|
+
}
|
|
215
|
+
else if (key.pageUp) {
|
|
216
|
+
// Page up
|
|
217
|
+
setDetailScroll(Math.max(0, detailScroll - 10));
|
|
218
|
+
}
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
// Operations selection mode
|
|
222
|
+
if (input === 'q' || key.escape) {
|
|
223
|
+
console.clear();
|
|
224
|
+
onBack();
|
|
225
|
+
setSelectedOperation(0);
|
|
226
|
+
}
|
|
227
|
+
else if (input === 'i') {
|
|
228
|
+
setShowDetailedInfo(true);
|
|
229
|
+
setDetailScroll(0);
|
|
230
|
+
}
|
|
231
|
+
else if (input === 'o') {
|
|
232
|
+
// Open in browser
|
|
233
|
+
const url = `https://platform.runloop.ai/devboxes/${selectedDevbox.id}`;
|
|
234
|
+
const openBrowser = async () => {
|
|
235
|
+
const { exec } = await import('child_process');
|
|
236
|
+
const platform = process.platform;
|
|
237
|
+
let openCommand;
|
|
238
|
+
if (platform === 'darwin') {
|
|
239
|
+
openCommand = `open "${url}"`;
|
|
240
|
+
}
|
|
241
|
+
else if (platform === 'win32') {
|
|
242
|
+
openCommand = `start "${url}"`;
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
openCommand = `xdg-open "${url}"`;
|
|
246
|
+
}
|
|
247
|
+
exec(openCommand);
|
|
248
|
+
};
|
|
249
|
+
openBrowser();
|
|
250
|
+
}
|
|
251
|
+
else if (key.upArrow && selectedOperation > 0) {
|
|
252
|
+
setSelectedOperation(selectedOperation - 1);
|
|
253
|
+
}
|
|
254
|
+
else if (key.downArrow && selectedOperation < operations.length - 1) {
|
|
255
|
+
setSelectedOperation(selectedOperation + 1);
|
|
256
|
+
}
|
|
257
|
+
else if (key.return) {
|
|
258
|
+
console.clear();
|
|
259
|
+
const op = operations[selectedOperation].key;
|
|
260
|
+
setExecutingOperation(op);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
const executeOperation = async () => {
|
|
264
|
+
const client = getClient();
|
|
265
|
+
const devbox = selectedDevbox;
|
|
266
|
+
try {
|
|
267
|
+
setLoading(true);
|
|
268
|
+
switch (executingOperation) {
|
|
269
|
+
case 'exec':
|
|
270
|
+
const execResult = await client.devboxes.executeSync(devbox.id, {
|
|
271
|
+
command: operationInput,
|
|
272
|
+
});
|
|
273
|
+
setOperationResult(execResult.stdout || execResult.stderr || 'Command executed');
|
|
274
|
+
break;
|
|
275
|
+
case 'upload':
|
|
276
|
+
// For upload, operationInput should be file path
|
|
277
|
+
const fs = await import('fs');
|
|
278
|
+
const fileStream = fs.createReadStream(operationInput);
|
|
279
|
+
const filename = operationInput.split('/').pop() || 'file';
|
|
280
|
+
await client.devboxes.uploadFile(devbox.id, {
|
|
281
|
+
path: filename,
|
|
282
|
+
file: fileStream,
|
|
283
|
+
});
|
|
284
|
+
setOperationResult(`File ${filename} uploaded successfully`);
|
|
285
|
+
break;
|
|
286
|
+
case 'snapshot':
|
|
287
|
+
const snapshot = await client.devboxes.snapshotDisk(devbox.id, {
|
|
288
|
+
name: operationInput || `snapshot-${Date.now()}`,
|
|
289
|
+
});
|
|
290
|
+
setOperationResult(`Snapshot created: ${snapshot.id}`);
|
|
291
|
+
break;
|
|
292
|
+
case 'ssh':
|
|
293
|
+
const sshKey = await client.devboxes.createSSHKey(devbox.id);
|
|
294
|
+
// Save SSH key to persistent location
|
|
295
|
+
const fsModule = await import('fs');
|
|
296
|
+
const pathModule = await import('path');
|
|
297
|
+
const osModule = await import('os');
|
|
298
|
+
const sshDir = pathModule.join(osModule.homedir(), '.runloop', 'ssh_keys');
|
|
299
|
+
fsModule.mkdirSync(sshDir, { recursive: true });
|
|
300
|
+
const keyPath = pathModule.join(sshDir, `${devbox.id}.pem`);
|
|
301
|
+
fsModule.writeFileSync(keyPath, sshKey.ssh_private_key, { mode: 0o600 });
|
|
302
|
+
// Determine user from launch parameters
|
|
303
|
+
const sshUser = devbox.launch_parameters?.user_parameters?.username || 'user';
|
|
304
|
+
const proxyCommand = 'openssl s_client -quiet -verify_quiet -servername %h -connect ssh.runloop.ai:443 2>/dev/null';
|
|
305
|
+
// Store SSH command details globally
|
|
306
|
+
global.__sshCommand = {
|
|
307
|
+
keyPath,
|
|
308
|
+
proxyCommand,
|
|
309
|
+
sshUser,
|
|
310
|
+
url: sshKey.url,
|
|
311
|
+
devboxName: devbox.name || devbox.id
|
|
312
|
+
};
|
|
313
|
+
// Exit Ink app to release terminal, SSH will be spawned after exit
|
|
314
|
+
exit();
|
|
315
|
+
break;
|
|
316
|
+
case 'logs':
|
|
317
|
+
const logsResult = await client.devboxes.logs.list(devbox.id);
|
|
318
|
+
if (logsResult.logs.length === 0) {
|
|
319
|
+
setOperationResult('No logs available for this devbox.');
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
// Store logs data for custom rendering - show all logs
|
|
323
|
+
logsResult.__customRender = 'logs';
|
|
324
|
+
logsResult.__logs = logsResult.logs; // Show all logs, not just last 50
|
|
325
|
+
logsResult.__totalCount = logsResult.logs.length;
|
|
326
|
+
setOperationResult(logsResult);
|
|
327
|
+
}
|
|
328
|
+
break;
|
|
329
|
+
case 'tunnel':
|
|
330
|
+
const port = parseInt(operationInput);
|
|
331
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
332
|
+
setOperationError(new Error('Invalid port number. Please enter a port between 1 and 65535.'));
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
const tunnel = await client.devboxes.createTunnel(devbox.id, { port });
|
|
336
|
+
setOperationResult(`Tunnel created!\n\n` +
|
|
337
|
+
`Local Port: ${port}\n` +
|
|
338
|
+
`Public URL: ${tunnel.url}\n\n` +
|
|
339
|
+
`You can now access port ${port} on the devbox via:\n${tunnel.url}`);
|
|
340
|
+
}
|
|
341
|
+
break;
|
|
342
|
+
case 'suspend':
|
|
343
|
+
await client.devboxes.suspend(devbox.id);
|
|
344
|
+
setOperationResult(`Devbox ${devbox.id} suspended successfully`);
|
|
345
|
+
break;
|
|
346
|
+
case 'resume':
|
|
347
|
+
await client.devboxes.resume(devbox.id);
|
|
348
|
+
setOperationResult(`Devbox ${devbox.id} resumed successfully`);
|
|
349
|
+
break;
|
|
350
|
+
case 'delete':
|
|
351
|
+
await client.devboxes.shutdown(devbox.id);
|
|
352
|
+
setOperationResult(`Devbox ${devbox.id} shut down successfully`);
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
catch (err) {
|
|
357
|
+
setOperationError(err);
|
|
358
|
+
}
|
|
359
|
+
finally {
|
|
360
|
+
setLoading(false);
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
const uptime = selectedDevbox.create_time_ms
|
|
364
|
+
? Math.floor((Date.now() - selectedDevbox.create_time_ms) / 1000 / 60)
|
|
365
|
+
: null;
|
|
366
|
+
// Build detailed info lines for scrolling
|
|
367
|
+
const buildDetailLines = () => {
|
|
368
|
+
const lines = [];
|
|
369
|
+
const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
|
|
370
|
+
// Core Information
|
|
371
|
+
lines.push(_jsx(Text, { color: "yellow", bold: true, children: "Devbox Details" }, "core-title"));
|
|
372
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ID: ", selectedDevbox.id] }, "core-id"));
|
|
373
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" Name: ", selectedDevbox.name || '(none)'] }, "core-name"));
|
|
374
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" Status: ", capitalize(selectedDevbox.status)] }, "core-status"));
|
|
375
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" Created: ", new Date(selectedDevbox.create_time_ms).toLocaleString()] }, "core-created"));
|
|
376
|
+
if (selectedDevbox.end_time_ms) {
|
|
377
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" Ended: ", new Date(selectedDevbox.end_time_ms).toLocaleString()] }, "core-ended"));
|
|
378
|
+
}
|
|
379
|
+
lines.push(_jsx(Text, { children: " " }, "core-space"));
|
|
380
|
+
// Capabilities
|
|
381
|
+
if (selectedDevbox.capabilities && selectedDevbox.capabilities.length > 0) {
|
|
382
|
+
lines.push(_jsx(Text, { color: "yellow", bold: true, children: "Capabilities" }, "cap-title"));
|
|
383
|
+
selectedDevbox.capabilities.forEach((cap, idx) => {
|
|
384
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", figures.pointer, " ", cap] }, `cap-${idx}`));
|
|
385
|
+
});
|
|
386
|
+
lines.push(_jsx(Text, { children: " " }, "cap-space"));
|
|
387
|
+
}
|
|
388
|
+
// Launch Parameters
|
|
389
|
+
if (selectedDevbox.launch_parameters) {
|
|
390
|
+
lines.push(_jsx(Text, { color: "yellow", bold: true, children: "Launch Parameters" }, "launch-title"));
|
|
391
|
+
const lp = selectedDevbox.launch_parameters;
|
|
392
|
+
if (lp.resource_size_request) {
|
|
393
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" Resource Size Request: ", lp.resource_size_request] }, "launch-size-req"));
|
|
394
|
+
}
|
|
395
|
+
if (lp.architecture) {
|
|
396
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" Architecture: ", lp.architecture] }, "launch-arch"));
|
|
397
|
+
}
|
|
398
|
+
if (lp.custom_cpu_cores) {
|
|
399
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" CPU Cores: ", lp.custom_cpu_cores] }, "launch-cpu"));
|
|
400
|
+
}
|
|
401
|
+
if (lp.custom_gb_memory) {
|
|
402
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" Memory: ", lp.custom_gb_memory, "GB"] }, "launch-memory"));
|
|
403
|
+
}
|
|
404
|
+
if (lp.custom_disk_size) {
|
|
405
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" Disk Size: ", lp.custom_disk_size, "GB"] }, "launch-disk"));
|
|
406
|
+
}
|
|
407
|
+
if (lp.keep_alive_time_seconds) {
|
|
408
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" Keep Alive: ", lp.keep_alive_time_seconds, "s (", Math.floor(lp.keep_alive_time_seconds / 60), "m)"] }, "launch-keepalive"));
|
|
409
|
+
}
|
|
410
|
+
if (lp.after_idle) {
|
|
411
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" After Idle: ", lp.after_idle.on_idle, " after ", lp.after_idle.idle_time_seconds, "s"] }, "launch-afteridle"));
|
|
412
|
+
}
|
|
413
|
+
if (lp.available_ports && lp.available_ports.length > 0) {
|
|
414
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" Available Ports: ", lp.available_ports.join(', ')] }, "launch-ports"));
|
|
415
|
+
}
|
|
416
|
+
if (lp.launch_commands && lp.launch_commands.length > 0) {
|
|
417
|
+
lines.push(_jsx(Text, { dimColor: true, children: " Launch Commands:" }, "launch-launch-cmds"));
|
|
418
|
+
lp.launch_commands.forEach((cmd, idx) => {
|
|
419
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", figures.pointer, " ", cmd] }, `launch-cmd-${idx}`));
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
if (lp.required_services && lp.required_services.length > 0) {
|
|
423
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" Required Services: ", lp.required_services.join(', ')] }, "launch-services"));
|
|
424
|
+
}
|
|
425
|
+
if (lp.user_parameters) {
|
|
426
|
+
lines.push(_jsx(Text, { dimColor: true, children: " User Parameters:" }, "launch-user"));
|
|
427
|
+
if (lp.user_parameters.username) {
|
|
428
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" Username: ", lp.user_parameters.username] }, "user-name"));
|
|
429
|
+
}
|
|
430
|
+
if (lp.user_parameters.uid) {
|
|
431
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" UID: ", lp.user_parameters.uid] }, "user-uid"));
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
lines.push(_jsx(Text, { children: " " }, "launch-space"));
|
|
435
|
+
}
|
|
436
|
+
// Source
|
|
437
|
+
if (selectedDevbox.blueprint_id || selectedDevbox.snapshot_id) {
|
|
438
|
+
lines.push(_jsx(Text, { color: "yellow", bold: true, children: "Source" }, "source-title"));
|
|
439
|
+
if (selectedDevbox.blueprint_id) {
|
|
440
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" Blueprint: ", selectedDevbox.blueprint_id] }, "source-bp"));
|
|
441
|
+
}
|
|
442
|
+
if (selectedDevbox.snapshot_id) {
|
|
443
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" Snapshot: ", selectedDevbox.snapshot_id] }, "source-snap"));
|
|
444
|
+
}
|
|
445
|
+
lines.push(_jsx(Text, { children: " " }, "source-space"));
|
|
446
|
+
}
|
|
447
|
+
// Initiator
|
|
448
|
+
if (selectedDevbox.initiator_type) {
|
|
449
|
+
lines.push(_jsx(Text, { color: "yellow", bold: true, children: "Initiator" }, "init-title"));
|
|
450
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" Type: ", selectedDevbox.initiator_type] }, "init-type"));
|
|
451
|
+
if (selectedDevbox.initiator_id) {
|
|
452
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ID: ", selectedDevbox.initiator_id] }, "init-id"));
|
|
453
|
+
}
|
|
454
|
+
lines.push(_jsx(Text, { children: " " }, "init-space"));
|
|
455
|
+
}
|
|
456
|
+
// Status Details
|
|
457
|
+
if (selectedDevbox.failure_reason || selectedDevbox.shutdown_reason) {
|
|
458
|
+
lines.push(_jsx(Text, { color: "yellow", bold: true, children: "Status Details" }, "status-title"));
|
|
459
|
+
if (selectedDevbox.failure_reason) {
|
|
460
|
+
lines.push(_jsxs(Text, { color: "red", dimColor: true, children: [" Failure Reason: ", selectedDevbox.failure_reason] }, "status-fail"));
|
|
461
|
+
}
|
|
462
|
+
if (selectedDevbox.shutdown_reason) {
|
|
463
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" Shutdown Reason: ", selectedDevbox.shutdown_reason] }, "status-shut"));
|
|
464
|
+
}
|
|
465
|
+
lines.push(_jsx(Text, { children: " " }, "status-space"));
|
|
466
|
+
}
|
|
467
|
+
// Metadata
|
|
468
|
+
if (selectedDevbox.metadata && Object.keys(selectedDevbox.metadata).length > 0) {
|
|
469
|
+
lines.push(_jsx(Text, { color: "yellow", bold: true, children: "Metadata" }, "meta-title"));
|
|
470
|
+
Object.entries(selectedDevbox.metadata).forEach(([key, value], idx) => {
|
|
471
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", key, ": ", value] }, `meta-${idx}`));
|
|
472
|
+
});
|
|
473
|
+
lines.push(_jsx(Text, { children: " " }, "meta-space"));
|
|
474
|
+
}
|
|
475
|
+
// State Transitions
|
|
476
|
+
if (selectedDevbox.state_transitions && selectedDevbox.state_transitions.length > 0) {
|
|
477
|
+
lines.push(_jsx(Text, { color: "yellow", bold: true, children: "State History" }, "state-title"));
|
|
478
|
+
selectedDevbox.state_transitions.forEach((transition, idx) => {
|
|
479
|
+
const text = `${idx + 1}. ${capitalize(transition.status)}${transition.transition_time_ms ? ` at ${new Date(transition.transition_time_ms).toLocaleString()}` : ''}`;
|
|
480
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", text] }, `state-${idx}`));
|
|
481
|
+
});
|
|
482
|
+
lines.push(_jsx(Text, { children: " " }, "state-space"));
|
|
483
|
+
}
|
|
484
|
+
// Raw JSON (full)
|
|
485
|
+
lines.push(_jsx(Text, { color: "yellow", bold: true, children: "Raw JSON" }, "json-title"));
|
|
486
|
+
const jsonLines = JSON.stringify(selectedDevbox, null, 2).split('\n');
|
|
487
|
+
jsonLines.forEach((line, idx) => {
|
|
488
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", line] }, `json-${idx}`));
|
|
489
|
+
});
|
|
490
|
+
return lines;
|
|
491
|
+
};
|
|
492
|
+
// Operation result display
|
|
493
|
+
if (operationResult || operationError) {
|
|
494
|
+
const operationLabel = operations.find((o) => o.key === executingOperation)?.label || 'Operation';
|
|
495
|
+
// Check for custom logs rendering
|
|
496
|
+
if (operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
|
|
497
|
+
const logs = operationResult.__logs || [];
|
|
498
|
+
const totalCount = operationResult.__totalCount || 0;
|
|
499
|
+
// Calculate viewport for scrolling
|
|
500
|
+
const terminalHeight = stdout?.rows || 30;
|
|
501
|
+
const terminalWidth = stdout?.columns || 120;
|
|
502
|
+
const viewportHeight = Math.max(10, terminalHeight - 10); // Reserve space for header/footer
|
|
503
|
+
const maxScroll = Math.max(0, logs.length - viewportHeight);
|
|
504
|
+
const actualScroll = Math.min(logsScroll, maxScroll);
|
|
505
|
+
const visibleLogs = logs.slice(actualScroll, actualScroll + viewportHeight);
|
|
506
|
+
const hasMore = actualScroll + viewportHeight < logs.length;
|
|
507
|
+
const hasLess = actualScroll > 0;
|
|
508
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
509
|
+
{ label: 'Devboxes' },
|
|
510
|
+
{ label: selectedDevbox?.name || selectedDevbox?.id || 'Devbox' },
|
|
511
|
+
{ label: 'Logs', active: true }
|
|
512
|
+
] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [visibleLogs.map((log, index) => {
|
|
513
|
+
const time = new Date(log.timestamp_ms).toLocaleTimeString();
|
|
514
|
+
const level = log.level ? log.level[0].toUpperCase() : 'I';
|
|
515
|
+
const source = log.source ? log.source.substring(0, 8) : 'exec';
|
|
516
|
+
const fullMessage = log.message || '';
|
|
517
|
+
const cmd = log.cmd ? `[${log.cmd.substring(0, 40)}${log.cmd.length > 40 ? '...' : ''}] ` : '';
|
|
518
|
+
const exitCode = log.exit_code !== null && log.exit_code !== undefined ? `(${log.exit_code}) ` : '';
|
|
519
|
+
let levelColor = 'gray';
|
|
520
|
+
if (level === 'E')
|
|
521
|
+
levelColor = 'red';
|
|
522
|
+
else if (level === 'W')
|
|
523
|
+
levelColor = 'yellow';
|
|
524
|
+
else if (level === 'I')
|
|
525
|
+
levelColor = 'cyan';
|
|
526
|
+
if (logsWrapMode) {
|
|
527
|
+
// Wrap mode: show full message on same line, let terminal handle wrapping
|
|
528
|
+
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));
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
// No-wrap mode: calculate actual metadata width and truncate accordingly
|
|
532
|
+
// Time (11) + space (1) + Level (1) + /source (1+8) + space (1) + exitCode.length + cmd.length + border/padding (6)
|
|
533
|
+
const metadataWidth = 11 + 1 + 1 + 1 + 8 + 1 + exitCode.length + cmd.length + 6;
|
|
534
|
+
const availableMessageWidth = Math.max(20, terminalWidth - metadataWidth);
|
|
535
|
+
const truncatedMessage = fullMessage.length > availableMessageWidth
|
|
536
|
+
? fullMessage.substring(0, availableMessageWidth - 3) + '...'
|
|
537
|
+
: fullMessage;
|
|
538
|
+
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));
|
|
539
|
+
}
|
|
540
|
+
}), 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"] }) })] }));
|
|
541
|
+
}
|
|
542
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
543
|
+
{ label: 'Devboxes' },
|
|
544
|
+
{ label: selectedDevbox?.name || selectedDevbox?.id || 'Devbox' },
|
|
545
|
+
{ label: operationLabel, active: true }
|
|
546
|
+
] }), _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" }) })] }));
|
|
547
|
+
}
|
|
548
|
+
// Operation input mode
|
|
549
|
+
if (executingOperation && selectedDevbox) {
|
|
550
|
+
const needsInput = executingOperation === 'exec' ||
|
|
551
|
+
executingOperation === 'upload' ||
|
|
552
|
+
executingOperation === 'snapshot' ||
|
|
553
|
+
executingOperation === 'tunnel';
|
|
554
|
+
const operationLabel = operations.find((o) => o.key === executingOperation)?.label || 'Operation';
|
|
555
|
+
if (loading) {
|
|
556
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
557
|
+
{ label: 'Devboxes' },
|
|
558
|
+
{ label: selectedDevbox.name || selectedDevbox.id },
|
|
559
|
+
{ label: operationLabel, active: true }
|
|
560
|
+
] }), _jsx(Header, { title: "Executing Operation" }), _jsx(SpinnerComponent, { message: "Please wait..." })] }));
|
|
561
|
+
}
|
|
562
|
+
if (!needsInput) {
|
|
563
|
+
// SSH, Logs, Suspend, Resume, and Delete operations are auto-executed via useEffect
|
|
564
|
+
const messages = {
|
|
565
|
+
ssh: 'Creating SSH key...',
|
|
566
|
+
logs: 'Fetching logs...',
|
|
567
|
+
suspend: 'Suspending devbox...',
|
|
568
|
+
resume: 'Resuming devbox...',
|
|
569
|
+
delete: 'Shutting down devbox...',
|
|
570
|
+
};
|
|
571
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
572
|
+
{ label: 'Devboxes' },
|
|
573
|
+
{ label: selectedDevbox.name || selectedDevbox.id },
|
|
574
|
+
{ label: operationLabel, active: true }
|
|
575
|
+
] }), _jsx(Header, { title: "Executing Operation" }), _jsx(SpinnerComponent, { message: messages[executingOperation] || 'Please wait...' })] }));
|
|
576
|
+
}
|
|
577
|
+
const prompts = {
|
|
578
|
+
exec: 'Command to execute:',
|
|
579
|
+
upload: 'File path to upload:',
|
|
580
|
+
snapshot: 'Snapshot name (optional):',
|
|
581
|
+
tunnel: 'Port number to expose:',
|
|
582
|
+
};
|
|
583
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
584
|
+
{ label: 'Devboxes' },
|
|
585
|
+
{ label: selectedDevbox.name || selectedDevbox.id },
|
|
586
|
+
{ label: operationLabel, active: true }
|
|
587
|
+
] }), _jsx(Header, { title: operationLabel }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "cyan", bold: true, children: selectedDevbox.name || selectedDevbox.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'
|
|
588
|
+
? 'ls -la'
|
|
589
|
+
: executingOperation === 'upload'
|
|
590
|
+
? '/path/to/file'
|
|
591
|
+
: executingOperation === 'tunnel'
|
|
592
|
+
? '8080'
|
|
593
|
+
: 'my-snapshot' }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "Press [Enter] to execute \u2022 [q or esc] Cancel" }) })] })] }));
|
|
594
|
+
}
|
|
595
|
+
// Detailed info mode - full screen
|
|
596
|
+
if (showDetailedInfo) {
|
|
597
|
+
const detailLines = buildDetailLines();
|
|
598
|
+
const terminalHeight = stdout?.rows || 30;
|
|
599
|
+
const viewportHeight = Math.max(10, terminalHeight - 12); // Reserve space for header/footer
|
|
600
|
+
const maxScroll = Math.max(0, detailLines.length - viewportHeight);
|
|
601
|
+
const actualScroll = Math.min(detailScroll, maxScroll);
|
|
602
|
+
const visibleLines = detailLines.slice(actualScroll, actualScroll + viewportHeight);
|
|
603
|
+
const hasMore = actualScroll + viewportHeight < detailLines.length;
|
|
604
|
+
const hasLess = actualScroll > 0;
|
|
605
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
606
|
+
{ label: 'Devboxes' },
|
|
607
|
+
{ label: selectedDevbox.name || selectedDevbox.id },
|
|
608
|
+
{ label: 'Full Details', active: true }
|
|
609
|
+
] }), _jsx(Header, { title: `${selectedDevbox.name || selectedDevbox.id} - Complete Information` }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: _jsxs(Box, { marginBottom: 1, children: [_jsx(StatusBadge, { status: selectedDevbox.status }), _jsx(Text, { children: " " }), _jsx(Text, { color: "gray", dimColor: true, children: selectedDevbox.id })] }) }), _jsxs(Box, { flexDirection: "column", marginTop: 1, marginBottom: 1, borderStyle: "round", borderColor: "gray", paddingX: 2, paddingY: 1, children: [_jsx(Box, { flexDirection: "column", children: visibleLines }), 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"] }) }))] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "gray", dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Scroll \u2022 [q or esc] Back to Operations \u2022 Line ", actualScroll + 1, "-", Math.min(actualScroll + viewportHeight, detailLines.length), " of ", detailLines.length] }) })] }));
|
|
610
|
+
}
|
|
611
|
+
// Operations selection mode (main detail view)
|
|
612
|
+
const lp = selectedDevbox.launch_parameters;
|
|
613
|
+
const hasCapabilities = selectedDevbox.capabilities && selectedDevbox.capabilities.filter((c) => c !== 'unknown').length > 0;
|
|
614
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
615
|
+
{ label: 'Devboxes' },
|
|
616
|
+
{ label: selectedDevbox.name || selectedDevbox.id, active: true }
|
|
617
|
+
] }), _jsx(Header, { title: "Devbox Details" }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, paddingY: 0, children: [_jsxs(Box, { children: [_jsx(Text, { color: "cyan", bold: true, children: selectedDevbox.name || selectedDevbox.id }), _jsx(Text, { children: " " }), _jsx(StatusBadge, { status: selectedDevbox.status }), _jsxs(Text, { color: "gray", dimColor: true, children: [" \u2022 ", selectedDevbox.id] })] }), _jsxs(Box, { children: [_jsx(Text, { color: "gray", dimColor: true, children: formattedCreateTime }), _jsxs(Text, { color: "gray", dimColor: true, children: [" (", createTimeAgo, ")"] })] }), uptime !== null && selectedDevbox.status === 'running' && (_jsxs(Box, { children: [_jsxs(Text, { color: "green", dimColor: true, children: ["Uptime: ", uptime < 60 ? `${uptime}m` : `${Math.floor(uptime / 60)}h ${uptime % 60}m`] }), lp?.keep_alive_time_seconds && (_jsxs(Text, { color: "gray", dimColor: true, children: [" \u2022 Keep-alive: ", Math.floor(lp.keep_alive_time_seconds / 60), "m"] }))] }))] }), _jsxs(Box, { flexDirection: "row", gap: 1, children: [(lp?.resource_size_request || lp?.custom_cpu_cores || lp?.custom_gb_memory || lp?.custom_disk_size || lp?.architecture) && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, paddingY: 0, flexGrow: 1, children: [_jsxs(Text, { color: "yellow", bold: true, children: [figures.squareSmallFilled, " Resources"] }), _jsxs(Text, { dimColor: true, children: [lp?.resource_size_request && `${lp.resource_size_request}`, lp?.architecture && ` • ${lp.architecture}`, lp?.custom_cpu_cores && ` • ${lp.custom_cpu_cores}VCPU`, lp?.custom_gb_memory && ` • ${lp.custom_gb_memory}GB RAM`, lp?.custom_disk_size && ` • ${lp.custom_disk_size}GB DISC`] })] })), hasCapabilities && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "blue", paddingX: 1, paddingY: 0, flexGrow: 1, children: [_jsxs(Text, { color: "blue", bold: true, children: [figures.tick, " Capabilities"] }), _jsx(Text, { dimColor: true, children: selectedDevbox.capabilities.filter((c) => c !== 'unknown').join(', ') })] })), (selectedDevbox.blueprint_id || selectedDevbox.snapshot_id) && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "magenta", paddingX: 1, paddingY: 0, flexGrow: 1, children: [_jsxs(Text, { color: "magenta", bold: true, children: [figures.circleFilled, " Source"] }), _jsxs(Text, { dimColor: true, children: [selectedDevbox.blueprint_id && `BP: ${selectedDevbox.blueprint_id}`, selectedDevbox.snapshot_id && `Snap: ${selectedDevbox.snapshot_id}`] })] }))] }), selectedDevbox.metadata && Object.keys(selectedDevbox.metadata).length > 0 && (_jsx(Box, { borderStyle: "round", borderColor: "green", paddingX: 1, paddingY: 0, children: _jsx(MetadataDisplay, { metadata: selectedDevbox.metadata, showBorder: false }) })), selectedDevbox.failure_reason && (_jsxs(Box, { borderStyle: "round", borderColor: "red", paddingX: 1, paddingY: 0, children: [_jsxs(Text, { color: "red", bold: true, children: [figures.cross, " "] }), _jsx(Text, { color: "red", dimColor: true, children: selectedDevbox.failure_reason })] })), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "cyan", bold: true, children: [figures.play, " Operations"] }), _jsx(Box, { flexDirection: "column", children: operations.map((op, index) => {
|
|
618
|
+
const isSelected = index === selectedOperation;
|
|
619
|
+
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] })] }, op.key));
|
|
620
|
+
}) })] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "gray", dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [Enter] Select \u2022 [i] Full Details \u2022 [o] Browser \u2022 [q] Back"] }) })] }));
|
|
621
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import figures from 'figures';
|
|
4
|
+
export const ErrorMessage = ({ message, error }) => {
|
|
5
|
+
return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Box, { children: _jsxs(Text, { color: "red", bold: true, children: [figures.cross, " ", message] }) }), error && (_jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: "gray", dimColor: true, children: error.message }) }))] }));
|
|
6
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
export const Header = React.memo(({ title, subtitle }) => {
|
|
5
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { bold: true, color: "#0a4d3a", children: ["\u258C", title] }), subtitle && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: "gray", dimColor: true, children: subtitle })] }))] }), _jsx(Box, { marginLeft: 1, children: _jsx(Text, { color: "#0a4d3a", children: '─'.repeat(title.length + 1) }) })] }));
|
|
6
|
+
});
|