@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,302 @@
|
|
|
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 figures from 'figures';
|
|
5
|
+
import { getClient } from '../../utils/client.js';
|
|
6
|
+
import { SpinnerComponent } from '../../components/Spinner.js';
|
|
7
|
+
import { ErrorMessage } from '../../components/ErrorMessage.js';
|
|
8
|
+
import { getStatusDisplay } from '../../components/StatusBadge.js';
|
|
9
|
+
import { Breadcrumb } from '../../components/Breadcrumb.js';
|
|
10
|
+
import { Table, createTextColumn } from '../../components/Table.js';
|
|
11
|
+
import { createExecutor } from '../../utils/CommandExecutor.js';
|
|
12
|
+
import { DevboxDetailPage } from '../../components/DevboxDetailPage.js';
|
|
13
|
+
import { DevboxCreatePage } from '../../components/DevboxCreatePage.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
|
+
const MAX_FETCH = 100;
|
|
35
|
+
const DEFAULT_PAGE_SIZE = 10;
|
|
36
|
+
const ListDevboxesUI = ({ status }) => {
|
|
37
|
+
const { exit } = useApp();
|
|
38
|
+
const { stdout } = useStdout();
|
|
39
|
+
const [loading, setLoading] = React.useState(true);
|
|
40
|
+
const [devboxes, setDevboxes] = React.useState([]);
|
|
41
|
+
const [error, setError] = React.useState(null);
|
|
42
|
+
const [currentPage, setCurrentPage] = React.useState(0);
|
|
43
|
+
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
|
44
|
+
const [showDetails, setShowDetails] = React.useState(false);
|
|
45
|
+
const [showCreate, setShowCreate] = React.useState(false);
|
|
46
|
+
const [refreshing, setRefreshing] = React.useState(false);
|
|
47
|
+
const [refreshIcon, setRefreshIcon] = React.useState(0);
|
|
48
|
+
// Calculate responsive dimensions
|
|
49
|
+
const terminalWidth = stdout?.columns || 120;
|
|
50
|
+
const terminalHeight = stdout?.rows || 30;
|
|
51
|
+
// Calculate dynamic page size based on terminal height
|
|
52
|
+
// Account for: Banner (3-4 lines) + Breadcrumb (1) + Header (1) + Stats (2) + Help text (2) + Margins (2) + Header row (1) = ~12 lines
|
|
53
|
+
const PAGE_SIZE = Math.max(5, terminalHeight - 12);
|
|
54
|
+
const fixedWidth = 4; // pointer + spaces
|
|
55
|
+
const statusIconWidth = 2;
|
|
56
|
+
const statusTextWidth = 10;
|
|
57
|
+
const timeWidth = 20;
|
|
58
|
+
const capabilitiesWidth = 18;
|
|
59
|
+
const tagWidth = 6;
|
|
60
|
+
// ID is always full width (25 chars for dbx_31CYd5LLFbBxst8mqnUjO format)
|
|
61
|
+
const idWidth = 26;
|
|
62
|
+
// Responsive layout based on terminal width
|
|
63
|
+
const showCapabilities = terminalWidth >= 120;
|
|
64
|
+
const showTags = terminalWidth >= 110;
|
|
65
|
+
// Name width is flexible and uses remaining space
|
|
66
|
+
let nameWidth = 15;
|
|
67
|
+
if (terminalWidth >= 120) {
|
|
68
|
+
const remainingWidth = terminalWidth - fixedWidth - statusIconWidth - idWidth - statusTextWidth - timeWidth - capabilitiesWidth - tagWidth - 12;
|
|
69
|
+
nameWidth = Math.max(15, remainingWidth);
|
|
70
|
+
}
|
|
71
|
+
else if (terminalWidth >= 110) {
|
|
72
|
+
const remainingWidth = terminalWidth - fixedWidth - statusIconWidth - idWidth - statusTextWidth - timeWidth - tagWidth - 10;
|
|
73
|
+
nameWidth = Math.max(12, remainingWidth);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
const remainingWidth = terminalWidth - fixedWidth - statusIconWidth - idWidth - statusTextWidth - timeWidth - 10;
|
|
77
|
+
nameWidth = Math.max(8, remainingWidth);
|
|
78
|
+
}
|
|
79
|
+
React.useEffect(() => {
|
|
80
|
+
const list = async (isInitialLoad = false) => {
|
|
81
|
+
try {
|
|
82
|
+
// Only show refreshing indicator on initial load
|
|
83
|
+
if (isInitialLoad) {
|
|
84
|
+
setRefreshing(true);
|
|
85
|
+
}
|
|
86
|
+
const client = getClient();
|
|
87
|
+
const allDevboxes = [];
|
|
88
|
+
let count = 0;
|
|
89
|
+
for await (const devbox of client.devboxes.list()) {
|
|
90
|
+
if (!status || devbox.status === status) {
|
|
91
|
+
allDevboxes.push(devbox);
|
|
92
|
+
}
|
|
93
|
+
count++;
|
|
94
|
+
if (count >= MAX_FETCH) {
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Only update if data actually changed
|
|
99
|
+
setDevboxes((prev) => {
|
|
100
|
+
const hasChanged = JSON.stringify(prev) !== JSON.stringify(allDevboxes);
|
|
101
|
+
return hasChanged ? allDevboxes : prev;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
setError(err);
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
setLoading(false);
|
|
109
|
+
// Show refresh indicator briefly
|
|
110
|
+
if (isInitialLoad) {
|
|
111
|
+
setTimeout(() => setRefreshing(false), 300);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
list(true);
|
|
116
|
+
// Poll every 3 seconds (increased from 2), but only when in list view
|
|
117
|
+
const interval = setInterval(() => {
|
|
118
|
+
if (!showDetails && !showCreate) {
|
|
119
|
+
list(false);
|
|
120
|
+
}
|
|
121
|
+
}, 3000);
|
|
122
|
+
return () => clearInterval(interval);
|
|
123
|
+
}, [showDetails, showCreate]);
|
|
124
|
+
// Animate refresh icon only when in list view
|
|
125
|
+
React.useEffect(() => {
|
|
126
|
+
if (showDetails || showCreate) {
|
|
127
|
+
return; // Don't animate when not in list view
|
|
128
|
+
}
|
|
129
|
+
const interval = setInterval(() => {
|
|
130
|
+
setRefreshIcon((prev) => (prev + 1) % 10);
|
|
131
|
+
}, 80);
|
|
132
|
+
return () => clearInterval(interval);
|
|
133
|
+
}, [showDetails, showCreate]);
|
|
134
|
+
useInput((input, key) => {
|
|
135
|
+
const pageDevboxes = currentDevboxes.length;
|
|
136
|
+
// Skip input handling when in details view - let DevboxDetailPage handle it
|
|
137
|
+
if (showDetails) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
// Skip input handling when in create view - let DevboxCreatePage handle it
|
|
141
|
+
if (showCreate) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
// Handle list view
|
|
145
|
+
if (key.upArrow && selectedIndex > 0) {
|
|
146
|
+
setSelectedIndex(selectedIndex - 1);
|
|
147
|
+
}
|
|
148
|
+
else if (key.downArrow && selectedIndex < pageDevboxes - 1) {
|
|
149
|
+
setSelectedIndex(selectedIndex + 1);
|
|
150
|
+
}
|
|
151
|
+
else if ((input === 'n' || key.rightArrow) && currentPage < totalPages - 1) {
|
|
152
|
+
setCurrentPage(currentPage + 1);
|
|
153
|
+
setSelectedIndex(0);
|
|
154
|
+
}
|
|
155
|
+
else if ((input === 'p' || key.leftArrow) && currentPage > 0) {
|
|
156
|
+
setCurrentPage(currentPage - 1);
|
|
157
|
+
setSelectedIndex(0);
|
|
158
|
+
}
|
|
159
|
+
else if (key.return) {
|
|
160
|
+
console.clear();
|
|
161
|
+
setShowDetails(true);
|
|
162
|
+
}
|
|
163
|
+
else if (input === 'c') {
|
|
164
|
+
console.clear();
|
|
165
|
+
setShowCreate(true);
|
|
166
|
+
}
|
|
167
|
+
else if (input === 'o' && selectedDevbox) {
|
|
168
|
+
// Open in browser
|
|
169
|
+
const url = `https://platform.runloop.ai/devboxes/${selectedDevbox.id}`;
|
|
170
|
+
const openBrowser = async () => {
|
|
171
|
+
const { exec } = await import('child_process');
|
|
172
|
+
const platform = process.platform;
|
|
173
|
+
let openCommand;
|
|
174
|
+
if (platform === 'darwin') {
|
|
175
|
+
openCommand = `open "${url}"`;
|
|
176
|
+
}
|
|
177
|
+
else if (platform === 'win32') {
|
|
178
|
+
openCommand = `start "${url}"`;
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
openCommand = `xdg-open "${url}"`;
|
|
182
|
+
}
|
|
183
|
+
exec(openCommand);
|
|
184
|
+
};
|
|
185
|
+
openBrowser();
|
|
186
|
+
}
|
|
187
|
+
else if (input === 'q') {
|
|
188
|
+
process.exit(0);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
const running = devboxes.filter((d) => d.status === 'running').length;
|
|
192
|
+
const stopped = devboxes.filter((d) => ['stopped', 'suspended'].includes(d.status)).length;
|
|
193
|
+
const totalPages = Math.ceil(devboxes.length / PAGE_SIZE);
|
|
194
|
+
const startIndex = currentPage * PAGE_SIZE;
|
|
195
|
+
const endIndex = Math.min(startIndex + PAGE_SIZE, devboxes.length);
|
|
196
|
+
const currentDevboxes = devboxes.slice(startIndex, endIndex);
|
|
197
|
+
const selectedDevbox = currentDevboxes[selectedIndex];
|
|
198
|
+
// Create view
|
|
199
|
+
if (showCreate) {
|
|
200
|
+
return (_jsx(DevboxCreatePage, { onBack: () => {
|
|
201
|
+
setShowCreate(false);
|
|
202
|
+
}, onCreate: (devbox) => {
|
|
203
|
+
// Refresh the list after creation
|
|
204
|
+
setShowCreate(false);
|
|
205
|
+
// The list will auto-refresh via the polling effect
|
|
206
|
+
} }));
|
|
207
|
+
}
|
|
208
|
+
// Details view
|
|
209
|
+
if (showDetails && selectedDevbox) {
|
|
210
|
+
return _jsx(DevboxDetailPage, { devbox: selectedDevbox, onBack: () => setShowDetails(false) });
|
|
211
|
+
}
|
|
212
|
+
// List view
|
|
213
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
214
|
+
{ label: 'Devboxes', active: true }
|
|
215
|
+
] }), loading && _jsx(SpinnerComponent, { message: "Loading..." }), !loading && !error && devboxes.length === 0 && (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: figures.info }), _jsx(Text, { children: " No devboxes found. Try: " }), _jsx(Text, { color: "cyan", bold: true, children: "rln devbox create" })] })), !loading && !error && devboxes.length > 0 && (_jsxs(_Fragment, { children: [_jsx(Table, { data: currentDevboxes, keyExtractor: (devbox) => devbox.id, selectedIndex: selectedIndex, title: `devboxes[${devboxes.length}]`, columns: [
|
|
216
|
+
{
|
|
217
|
+
key: 'statusIcon',
|
|
218
|
+
label: '',
|
|
219
|
+
width: statusIconWidth,
|
|
220
|
+
render: (devbox, index, isSelected) => {
|
|
221
|
+
const statusDisplay = getStatusDisplay(devbox.status);
|
|
222
|
+
const status = devbox.status;
|
|
223
|
+
let color = 'gray';
|
|
224
|
+
if (status === 'running')
|
|
225
|
+
color = 'green';
|
|
226
|
+
else if (status === 'stopped' || status === 'suspended')
|
|
227
|
+
color = 'gray';
|
|
228
|
+
else if (status === 'starting' || status === 'stopping')
|
|
229
|
+
color = 'yellow';
|
|
230
|
+
else if (status === 'failed')
|
|
231
|
+
color = 'red';
|
|
232
|
+
const padded = statusDisplay.icon.padEnd(statusIconWidth, ' ');
|
|
233
|
+
return (_jsx(Text, { color: isSelected ? 'white' : color, bold: true, inverse: isSelected, children: padded }));
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
createTextColumn('id', 'ID', (devbox) => devbox.id, { width: idWidth, color: 'gray', dimColor: true, bold: false }),
|
|
237
|
+
{
|
|
238
|
+
key: 'statusText',
|
|
239
|
+
label: 'Status',
|
|
240
|
+
width: statusTextWidth,
|
|
241
|
+
render: (devbox, index, isSelected) => {
|
|
242
|
+
const statusDisplay = getStatusDisplay(devbox.status);
|
|
243
|
+
const status = devbox.status;
|
|
244
|
+
let color = 'gray';
|
|
245
|
+
if (status === 'running')
|
|
246
|
+
color = 'green';
|
|
247
|
+
else if (status === 'stopped' || status === 'suspended')
|
|
248
|
+
color = 'gray';
|
|
249
|
+
else if (status === 'starting' || status === 'stopping')
|
|
250
|
+
color = 'yellow';
|
|
251
|
+
else if (status === 'failed')
|
|
252
|
+
color = 'red';
|
|
253
|
+
const truncated = statusDisplay.text.slice(0, statusTextWidth);
|
|
254
|
+
const padded = truncated.padEnd(statusTextWidth, ' ');
|
|
255
|
+
return (_jsx(Text, { color: isSelected ? 'white' : color, bold: true, inverse: isSelected, children: padded }));
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
createTextColumn('name', 'Name', (devbox) => devbox.name || '', { width: nameWidth }),
|
|
259
|
+
createTextColumn('capabilities', 'Capabilities', (devbox) => {
|
|
260
|
+
const hasCapabilities = devbox.capabilities && devbox.capabilities.filter((c) => c !== 'unknown').length > 0;
|
|
261
|
+
return hasCapabilities
|
|
262
|
+
? `[${devbox.capabilities
|
|
263
|
+
.filter((c) => c !== 'unknown')
|
|
264
|
+
.map((c) => c === 'computer_usage' ? 'comp' : c === 'browser_usage' ? 'browser' : c === 'docker_in_docker' ? 'docker' : c)
|
|
265
|
+
.join(',')}]`
|
|
266
|
+
: '';
|
|
267
|
+
}, { width: capabilitiesWidth, color: 'blue', dimColor: true, bold: false, visible: showCapabilities }),
|
|
268
|
+
createTextColumn('tags', 'Tags', (devbox) => devbox.blueprint_id ? '[bp]' : devbox.snapshot_id ? '[snap]' : '', { width: tagWidth, color: 'yellow', dimColor: true, bold: false, visible: showTags }),
|
|
269
|
+
createTextColumn('created', 'Created', (devbox) => devbox.create_time_ms ? formatTimeAgo(devbox.create_time_ms) : '', { width: timeWidth, color: 'gray', dimColor: true, bold: false }),
|
|
270
|
+
] }), _jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: "cyan", bold: true, children: [figures.hamburger, " ", devboxes.length, devboxes.length >= MAX_FETCH && '+'] }), _jsx(Text, { color: "gray", dimColor: true, children: " total" }), totalPages > 1 && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", dimColor: true, children: " \u2022 " }), _jsxs(Text, { color: "gray", dimColor: true, children: ["Page ", currentPage + 1, " of ", totalPages] })] })), _jsx(Text, { color: "gray", dimColor: true, children: " \u2022 " }), _jsxs(Text, { color: "gray", dimColor: true, children: ["Showing ", startIndex + 1, "-", endIndex, " of ", devboxes.length] }), _jsx(Text, { children: " " }), refreshing ? (_jsx(Text, { color: "cyan", children: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'][refreshIcon % 10] })) : (_jsx(Text, { color: "green", children: figures.circleFilled }))] }), _jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: "gray", dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate"] }), totalPages > 1 && (_jsxs(Text, { color: "gray", dimColor: true, children: [' ', "\u2022 ", figures.arrowLeft, figures.arrowRight, " Page"] })), _jsxs(Text, { color: "gray", dimColor: true, children: [' ', "\u2022 [Enter] Operations \u2022 [c] Create \u2022 [o] Browser \u2022 [q] Quit"] })] })] })), error && _jsx(ErrorMessage, { message: "Failed to list devboxes", error: error })] }));
|
|
271
|
+
};
|
|
272
|
+
export async function listDevboxes(options) {
|
|
273
|
+
const executor = createExecutor(options);
|
|
274
|
+
await executor.executeList(async () => {
|
|
275
|
+
const client = executor.getClient();
|
|
276
|
+
return executor.fetchFromIterator(client.devboxes.list(), {
|
|
277
|
+
filter: options.status ? (devbox) => devbox.status === options.status : undefined,
|
|
278
|
+
limit: DEFAULT_PAGE_SIZE,
|
|
279
|
+
});
|
|
280
|
+
}, () => _jsx(ListDevboxesUI, { status: options.status }), DEFAULT_PAGE_SIZE);
|
|
281
|
+
// Check if we need to spawn SSH after Ink exit
|
|
282
|
+
const sshCommand = global.__sshCommand;
|
|
283
|
+
if (sshCommand) {
|
|
284
|
+
delete global.__sshCommand;
|
|
285
|
+
// Import spawn
|
|
286
|
+
const { spawnSync } = await import('child_process');
|
|
287
|
+
// Clear and show connection message
|
|
288
|
+
console.clear();
|
|
289
|
+
console.log(`\nConnecting to devbox ${sshCommand.devboxName}...\n`);
|
|
290
|
+
// Spawn SSH in foreground
|
|
291
|
+
const result = spawnSync('ssh', [
|
|
292
|
+
'-i', sshCommand.keyPath,
|
|
293
|
+
'-o', `ProxyCommand=${sshCommand.proxyCommand}`,
|
|
294
|
+
'-o', 'StrictHostKeyChecking=no',
|
|
295
|
+
'-o', 'UserKnownHostsFile=/dev/null',
|
|
296
|
+
`${sshCommand.sshUser}@${sshCommand.url}`
|
|
297
|
+
], {
|
|
298
|
+
stdio: 'inherit'
|
|
299
|
+
});
|
|
300
|
+
process.exit(result.status || 0);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { render } from 'ink';
|
|
4
|
+
import { createReadStream } from 'fs';
|
|
5
|
+
import { getClient } from '../../utils/client.js';
|
|
6
|
+
import { Header } from '../../components/Header.js';
|
|
7
|
+
import { SpinnerComponent } from '../../components/Spinner.js';
|
|
8
|
+
import { SuccessMessage } from '../../components/SuccessMessage.js';
|
|
9
|
+
import { ErrorMessage } from '../../components/ErrorMessage.js';
|
|
10
|
+
const UploadFileUI = ({ id, file, targetPath }) => {
|
|
11
|
+
const [loading, setLoading] = React.useState(true);
|
|
12
|
+
const [success, setSuccess] = React.useState(false);
|
|
13
|
+
const [error, setError] = React.useState(null);
|
|
14
|
+
React.useEffect(() => {
|
|
15
|
+
const upload = async () => {
|
|
16
|
+
try {
|
|
17
|
+
const client = getClient();
|
|
18
|
+
const fileStream = createReadStream(file);
|
|
19
|
+
const filename = file.split('/').pop() || 'uploaded-file';
|
|
20
|
+
await client.devboxes.uploadFile(id, {
|
|
21
|
+
path: targetPath || filename,
|
|
22
|
+
file: fileStream,
|
|
23
|
+
});
|
|
24
|
+
setSuccess(true);
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
setError(err);
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
setLoading(false);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
upload();
|
|
34
|
+
}, []);
|
|
35
|
+
return (_jsxs(_Fragment, { children: [_jsx(Header, { title: "Upload File", subtitle: `Uploading to devbox: ${id}` }), loading && _jsx(SpinnerComponent, { message: "Uploading file..." }), success && (_jsx(SuccessMessage, { message: "File uploaded successfully!", details: `File: ${file}${targetPath ? `\nTarget: ${targetPath}` : ''}` })), error && _jsx(ErrorMessage, { message: "Failed to upload file", error: error })] }));
|
|
36
|
+
};
|
|
37
|
+
export async function uploadFile(id, file, options) {
|
|
38
|
+
const { waitUntilExit } = render(_jsx(UploadFileUI, { id: id, file: file, targetPath: options.path }));
|
|
39
|
+
await waitUntilExit();
|
|
40
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { render, Box, Text } from 'ink';
|
|
4
|
+
import { getClient } from '../utils/client.js';
|
|
5
|
+
import { Header } from '../components/Header.js';
|
|
6
|
+
import { SpinnerComponent } from '../components/Spinner.js';
|
|
7
|
+
import { ErrorMessage } from '../components/ErrorMessage.js';
|
|
8
|
+
const ExecCommandUI = ({ id, command, }) => {
|
|
9
|
+
const [loading, setLoading] = React.useState(true);
|
|
10
|
+
const [output, setOutput] = React.useState('');
|
|
11
|
+
const [error, setError] = React.useState(null);
|
|
12
|
+
React.useEffect(() => {
|
|
13
|
+
const exec = async () => {
|
|
14
|
+
try {
|
|
15
|
+
const client = getClient();
|
|
16
|
+
const result = await client.devboxes.executeSync(id, {
|
|
17
|
+
command: command.join(' '),
|
|
18
|
+
});
|
|
19
|
+
setOutput(result.stdout || result.stderr || 'Command executed successfully');
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
setError(err);
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
setLoading(false);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
exec();
|
|
29
|
+
}, []);
|
|
30
|
+
return (_jsxs(_Fragment, { children: [_jsx(Header, { title: "Execute Command", subtitle: `Running in devbox: ${id}` }), loading && _jsx(SpinnerComponent, { message: "Executing command..." }), !loading && !error && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(Box, { borderStyle: "round", borderColor: "green", padding: 1, children: _jsx(Text, { children: output }) }) })), error && _jsx(ErrorMessage, { message: "Failed to execute command", error: error })] }));
|
|
31
|
+
};
|
|
32
|
+
export async function execCommand(id, command) {
|
|
33
|
+
const { waitUntilExit } = render(_jsx(ExecCommandUI, { id: id, command: command }));
|
|
34
|
+
await waitUntilExit();
|
|
35
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { render, Box, Text, useInput } from 'ink';
|
|
4
|
+
import Gradient from 'ink-gradient';
|
|
5
|
+
import figures from 'figures';
|
|
6
|
+
import { getClient } from '../utils/client.js';
|
|
7
|
+
import { Header } from '../components/Header.js';
|
|
8
|
+
import { SpinnerComponent } from '../components/Spinner.js';
|
|
9
|
+
import { DevboxCard } from '../components/DevboxCard.js';
|
|
10
|
+
import { ErrorMessage } from '../components/ErrorMessage.js';
|
|
11
|
+
const PAGE_SIZE = 10;
|
|
12
|
+
const ListDevboxesUI = ({ status }) => {
|
|
13
|
+
const [loading, setLoading] = React.useState(true);
|
|
14
|
+
const [devboxes, setDevboxes] = React.useState([]);
|
|
15
|
+
const [error, setError] = React.useState(null);
|
|
16
|
+
const [currentPage, setCurrentPage] = React.useState(0);
|
|
17
|
+
React.useEffect(() => {
|
|
18
|
+
const list = async () => {
|
|
19
|
+
try {
|
|
20
|
+
const client = getClient();
|
|
21
|
+
const allDevboxes = [];
|
|
22
|
+
for await (const devbox of client.devboxes.list()) {
|
|
23
|
+
if (!status || devbox.status === status) {
|
|
24
|
+
allDevboxes.push(devbox);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
setDevboxes(allDevboxes);
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
setError(err);
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
setLoading(false);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
list();
|
|
37
|
+
}, []);
|
|
38
|
+
useInput((input, key) => {
|
|
39
|
+
if (input === 'n' && currentPage < totalPages - 1) {
|
|
40
|
+
setCurrentPage(currentPage + 1);
|
|
41
|
+
}
|
|
42
|
+
else if (input === 'p' && currentPage > 0) {
|
|
43
|
+
setCurrentPage(currentPage - 1);
|
|
44
|
+
}
|
|
45
|
+
else if (input === 'q') {
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
const getStatusCount = (status) => devboxes.filter((d) => d.status === status).length;
|
|
50
|
+
const totalPages = Math.ceil(devboxes.length / PAGE_SIZE);
|
|
51
|
+
const startIndex = currentPage * PAGE_SIZE;
|
|
52
|
+
const endIndex = Math.min(startIndex + PAGE_SIZE, devboxes.length);
|
|
53
|
+
const currentDevboxes = devboxes.slice(startIndex, endIndex);
|
|
54
|
+
return (_jsxs(_Fragment, { children: [_jsx(Header, { title: "Your Devboxes", subtitle: status ? `Filtering by status: ${status}` : 'Showing all devboxes' }), loading && _jsx(SpinnerComponent, { message: "Fetching your devboxes..." }), !loading && !error && devboxes.length === 0 && (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 3, paddingY: 2, marginY: 1, flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: "yellow", bold: true, children: [figures.info, " No devboxes found"] }) }), _jsxs(Box, { marginLeft: 2, children: [_jsx(Text, { color: "gray", children: "Create your first devbox with: " }), _jsx(Text, { color: "cyan", bold: true, children: "rln create" })] })] })), !loading && !error && devboxes.length > 0 && (_jsxs(_Fragment, { children: [_jsxs(Box, { borderStyle: "round", borderColor: "magenta", paddingX: 3, paddingY: 1, marginY: 1, flexDirection: "column", children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Box, { children: _jsx(Gradient, { name: "passion", children: _jsxs(Text, { bold: true, children: [figures.star, " Summary"] }) }) }), _jsx(Box, { children: _jsxs(Text, { color: "cyan", children: ["Page ", currentPage + 1, "/", totalPages, " (", startIndex + 1, "-", endIndex, " of", ' ', devboxes.length, ")"] }) })] }), _jsxs(Box, { marginTop: 1, gap: 3, children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "green", bold: true, children: figures.tick }), _jsx(Text, { color: "gray", children: "Running:" }), _jsx(Text, { color: "green", bold: true, children: getStatusCount('running') })] }), _jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "yellow", bold: true, children: figures.ellipsis }), _jsx(Text, { color: "gray", children: "Provisioning:" }), _jsx(Text, { color: "yellow", bold: true, children: getStatusCount('provisioning') + getStatusCount('initializing') })] }), _jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "gray", bold: true, children: figures.circleDotted }), _jsx(Text, { color: "gray", children: "Stopped:" }), _jsx(Text, { color: "gray", bold: true, children: getStatusCount('stopped') + getStatusCount('suspended') })] }), _jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "cyan", bold: true, children: figures.hamburger }), _jsx(Text, { color: "gray", children: "Total:" }), _jsx(Text, { color: "cyan", bold: true, children: devboxes.length })] })] })] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: currentDevboxes.map((devbox, index) => (_jsx(DevboxCard, { id: devbox.id, name: devbox.name, status: devbox.status, createdAt: devbox.created_at, index: startIndex + index }, devbox.id))) }), totalPages > 1 && (_jsx(Box, { borderStyle: "round", borderColor: "blue", paddingX: 3, paddingY: 1, marginTop: 1, flexDirection: "column", children: _jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Box, { gap: 3, children: [currentPage > 0 && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", bold: true, children: "[p]" }), _jsx(Text, { color: "gray", children: " Previous" })] })), currentPage < totalPages - 1 && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", bold: true, children: "[n]" }), _jsx(Text, { color: "gray", children: " Next" })] })), _jsxs(Box, { children: [_jsx(Text, { color: "red", bold: true, children: "[q]" }), _jsx(Text, { color: "gray", children: " Quit" })] })] }), _jsx(Box, { children: _jsxs(Text, { color: "gray", dimColor: true, children: [figures.arrowRight, " Press a key to navigate"] }) })] }) }))] })), error && _jsx(ErrorMessage, { message: "Failed to list devboxes", error: error })] }));
|
|
55
|
+
};
|
|
56
|
+
export async function listDevboxes(options) {
|
|
57
|
+
const { waitUntilExit } = render(_jsx(ListDevboxesUI, { status: options.status }));
|
|
58
|
+
await waitUntilExit();
|
|
59
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { render, Box, Text } from 'ink';
|
|
4
|
+
import Gradient from 'ink-gradient';
|
|
5
|
+
import figures from 'figures';
|
|
6
|
+
import { getClient } from '../../utils/client.js';
|
|
7
|
+
import { Header } from '../../components/Header.js';
|
|
8
|
+
import { Banner } from '../../components/Banner.js';
|
|
9
|
+
import { SpinnerComponent } from '../../components/Spinner.js';
|
|
10
|
+
import { SuccessMessage } from '../../components/SuccessMessage.js';
|
|
11
|
+
import { ErrorMessage } from '../../components/ErrorMessage.js';
|
|
12
|
+
const CreateSnapshotUI = ({ devboxId, name }) => {
|
|
13
|
+
const [loading, setLoading] = React.useState(true);
|
|
14
|
+
const [result, setResult] = React.useState(null);
|
|
15
|
+
const [error, setError] = React.useState(null);
|
|
16
|
+
React.useEffect(() => {
|
|
17
|
+
const create = async () => {
|
|
18
|
+
try {
|
|
19
|
+
const client = getClient();
|
|
20
|
+
const snapshot = await client.devboxes.snapshotDisk(devboxId, {
|
|
21
|
+
...(name && { name }),
|
|
22
|
+
});
|
|
23
|
+
setResult(snapshot);
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
setError(err);
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
setLoading(false);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
create();
|
|
33
|
+
}, []);
|
|
34
|
+
return (_jsxs(_Fragment, { children: [_jsx(Banner, {}), _jsx(Header, { title: "Create Snapshot", subtitle: "Taking a snapshot of your devbox..." }), loading && (_jsxs(_Fragment, { children: [_jsx(SpinnerComponent, { message: "Creating snapshot..." }), _jsxs(Box, { borderStyle: "round", borderColor: "blue", paddingX: 3, paddingY: 1, marginY: 1, flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: "cyan", bold: true, children: [figures.info, " Configuration"] }) }), _jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: "gray", children: [figures.pointer, " Devbox ID: "] }), _jsx(Text, { color: "white", children: devboxId })] }), name && (_jsxs(Box, { children: [_jsxs(Text, { color: "gray", children: [figures.pointer, " Name: "] }), _jsx(Text, { color: "white", children: name })] }))] })] })] })), result && (_jsxs(_Fragment, { children: [_jsx(SuccessMessage, { message: "Snapshot created successfully!", details: `ID: ${result.id}\nName: ${result.name || '(unnamed)'}\nStatus: ${result.status}` }), _jsxs(Box, { borderStyle: "double", borderColor: "green", paddingX: 3, paddingY: 1, marginY: 1, flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Gradient, { name: "summer", children: _jsxs(Text, { bold: true, children: [figures.star, " Next Steps"] }) }) }), _jsxs(Box, { flexDirection: "column", gap: 1, marginLeft: 2, children: [_jsxs(Box, { children: [_jsxs(Text, { color: "gray", children: [figures.tick, " View snapshots: "] }), _jsx(Text, { color: "cyan", children: "rln snapshot list" })] }), _jsxs(Box, { children: [_jsxs(Text, { color: "gray", children: [figures.tick, " Create devbox from snapshot: "] }), _jsxs(Text, { color: "cyan", children: ["rln devbox create -t ", result.id] })] })] })] })] })), error && _jsx(ErrorMessage, { message: "Failed to create snapshot", error: error })] }));
|
|
35
|
+
};
|
|
36
|
+
export async function createSnapshot(devboxId, options) {
|
|
37
|
+
console.clear();
|
|
38
|
+
const { waitUntilExit } = render(_jsx(CreateSnapshotUI, { devboxId: devboxId, name: options.name }));
|
|
39
|
+
await waitUntilExit();
|
|
40
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { getClient } from '../../utils/client.js';
|
|
4
|
+
import { Header } from '../../components/Header.js';
|
|
5
|
+
import { SpinnerComponent } from '../../components/Spinner.js';
|
|
6
|
+
import { SuccessMessage } from '../../components/SuccessMessage.js';
|
|
7
|
+
import { ErrorMessage } from '../../components/ErrorMessage.js';
|
|
8
|
+
import { createExecutor } from '../../utils/CommandExecutor.js';
|
|
9
|
+
const DeleteSnapshotUI = ({ id }) => {
|
|
10
|
+
const [loading, setLoading] = React.useState(true);
|
|
11
|
+
const [success, setSuccess] = React.useState(false);
|
|
12
|
+
const [error, setError] = React.useState(null);
|
|
13
|
+
React.useEffect(() => {
|
|
14
|
+
const deleteSnapshot = async () => {
|
|
15
|
+
try {
|
|
16
|
+
const client = getClient();
|
|
17
|
+
await client.devboxes.diskSnapshots.delete(id);
|
|
18
|
+
setSuccess(true);
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
setError(err);
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
setLoading(false);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
deleteSnapshot();
|
|
28
|
+
}, []);
|
|
29
|
+
return (_jsxs(_Fragment, { children: [_jsx(Header, { title: "Delete Snapshot", subtitle: `Deleting snapshot: ${id}` }), loading && _jsx(SpinnerComponent, { message: "Deleting snapshot..." }), success && (_jsx(SuccessMessage, { message: "Snapshot deleted successfully!", details: `ID: ${id}` })), error && _jsx(ErrorMessage, { message: "Failed to delete snapshot", error: error })] }));
|
|
30
|
+
};
|
|
31
|
+
export async function deleteSnapshot(id, options = {}) {
|
|
32
|
+
const executor = createExecutor(options);
|
|
33
|
+
await executor.executeDelete(async () => {
|
|
34
|
+
const client = executor.getClient();
|
|
35
|
+
await client.devboxes.diskSnapshots.delete(id);
|
|
36
|
+
}, id, () => _jsx(DeleteSnapshotUI, { id: id }));
|
|
37
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
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, useStdout } from 'ink';
|
|
4
|
+
import figures from 'figures';
|
|
5
|
+
import { getClient } from '../../utils/client.js';
|
|
6
|
+
import { Header } from '../../components/Header.js';
|
|
7
|
+
import { Banner } from '../../components/Banner.js';
|
|
8
|
+
import { SpinnerComponent } from '../../components/Spinner.js';
|
|
9
|
+
import { ErrorMessage } from '../../components/ErrorMessage.js';
|
|
10
|
+
import { StatusBadge } from '../../components/StatusBadge.js';
|
|
11
|
+
import { Breadcrumb } from '../../components/Breadcrumb.js';
|
|
12
|
+
import { Table, createTextColumn, createComponentColumn } from '../../components/Table.js';
|
|
13
|
+
import { createExecutor } from '../../utils/CommandExecutor.js';
|
|
14
|
+
const PAGE_SIZE = 10;
|
|
15
|
+
const MAX_FETCH = 100;
|
|
16
|
+
// Format time ago
|
|
17
|
+
const formatTimeAgo = (timestamp) => {
|
|
18
|
+
const seconds = Math.floor((Date.now() - timestamp) / 1000);
|
|
19
|
+
if (seconds < 60)
|
|
20
|
+
return `${seconds}s ago`;
|
|
21
|
+
const minutes = Math.floor(seconds / 60);
|
|
22
|
+
if (minutes < 60)
|
|
23
|
+
return `${minutes}m ago`;
|
|
24
|
+
const hours = Math.floor(minutes / 60);
|
|
25
|
+
if (hours < 24)
|
|
26
|
+
return `${hours}h ago`;
|
|
27
|
+
const days = Math.floor(hours / 24);
|
|
28
|
+
if (days < 30)
|
|
29
|
+
return `${days}d ago`;
|
|
30
|
+
const months = Math.floor(days / 30);
|
|
31
|
+
if (months < 12)
|
|
32
|
+
return `${months}mo ago`;
|
|
33
|
+
const years = Math.floor(months / 12);
|
|
34
|
+
return `${years}y ago`;
|
|
35
|
+
};
|
|
36
|
+
const ListSnapshotsUI = ({ devboxId }) => {
|
|
37
|
+
const { stdout } = useStdout();
|
|
38
|
+
const [loading, setLoading] = React.useState(true);
|
|
39
|
+
const [snapshots, setSnapshots] = React.useState([]);
|
|
40
|
+
const [error, setError] = React.useState(null);
|
|
41
|
+
const [currentPage, setCurrentPage] = React.useState(0);
|
|
42
|
+
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
|
43
|
+
// Calculate responsive column widths
|
|
44
|
+
const terminalWidth = stdout?.columns || 120;
|
|
45
|
+
const showDevboxId = terminalWidth >= 100 && !devboxId; // Hide devbox column if filtering by devbox
|
|
46
|
+
const showFullId = terminalWidth >= 80;
|
|
47
|
+
const idWidth = 25;
|
|
48
|
+
const nameWidth = terminalWidth >= 120 ? 30 : 25;
|
|
49
|
+
const devboxWidth = 15;
|
|
50
|
+
const timeWidth = 20;
|
|
51
|
+
React.useEffect(() => {
|
|
52
|
+
const list = async () => {
|
|
53
|
+
try {
|
|
54
|
+
const client = getClient();
|
|
55
|
+
const allSnapshots = [];
|
|
56
|
+
let count = 0;
|
|
57
|
+
const params = devboxId ? { devbox_id: devboxId } : {};
|
|
58
|
+
for await (const snapshot of client.devboxes.listDiskSnapshots(params)) {
|
|
59
|
+
allSnapshots.push(snapshot);
|
|
60
|
+
count++;
|
|
61
|
+
if (count >= MAX_FETCH) {
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
setSnapshots(allSnapshots);
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
setError(err);
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
setLoading(false);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
list();
|
|
75
|
+
}, [devboxId]);
|
|
76
|
+
useInput((input, key) => {
|
|
77
|
+
const pageSnapshots = currentSnapshots.length;
|
|
78
|
+
if (key.upArrow && selectedIndex > 0) {
|
|
79
|
+
setSelectedIndex(selectedIndex - 1);
|
|
80
|
+
}
|
|
81
|
+
else if (key.downArrow && selectedIndex < pageSnapshots - 1) {
|
|
82
|
+
setSelectedIndex(selectedIndex + 1);
|
|
83
|
+
}
|
|
84
|
+
else if ((input === 'n' || key.rightArrow) && currentPage < totalPages - 1) {
|
|
85
|
+
setCurrentPage(currentPage + 1);
|
|
86
|
+
setSelectedIndex(0);
|
|
87
|
+
}
|
|
88
|
+
else if ((input === 'p' || key.leftArrow) && currentPage > 0) {
|
|
89
|
+
setCurrentPage(currentPage - 1);
|
|
90
|
+
setSelectedIndex(0);
|
|
91
|
+
}
|
|
92
|
+
else if (input === 'q') {
|
|
93
|
+
process.exit(0);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
const totalPages = Math.ceil(snapshots.length / PAGE_SIZE);
|
|
97
|
+
const startIndex = currentPage * PAGE_SIZE;
|
|
98
|
+
const endIndex = Math.min(startIndex + PAGE_SIZE, snapshots.length);
|
|
99
|
+
const currentSnapshots = snapshots.slice(startIndex, endIndex);
|
|
100
|
+
const ready = snapshots.filter((s) => s.status === 'ready').length;
|
|
101
|
+
const pending = snapshots.filter((s) => s.status !== 'ready').length;
|
|
102
|
+
return (_jsxs(_Fragment, { children: [_jsx(Banner, {}), _jsx(Breadcrumb, { items: [
|
|
103
|
+
{ label: 'Snapshots', active: !devboxId },
|
|
104
|
+
...(devboxId ? [{ label: `Devbox: ${devboxId}`, active: true }] : []),
|
|
105
|
+
] }), _jsx(Header, { title: "Snapshots", subtitle: devboxId ? `Filtering by devbox: ${devboxId}` : undefined }), loading && _jsx(SpinnerComponent, { message: "Loading snapshots..." }), !loading && !error && snapshots.length === 0 && (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: figures.info }), _jsx(Text, { children: " No snapshots found. Try: " }), _jsx(Text, { color: "cyan", bold: true, children: "rln snapshot create <devbox-id>" })] })), !loading && !error && snapshots.length > 0 && (_jsxs(_Fragment, { children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: "green", children: [figures.tick, " ", ready] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: "yellow", children: [figures.ellipsis, " ", pending] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: "cyan", children: [figures.hamburger, " ", snapshots.length, snapshots.length >= MAX_FETCH && '+'] }), totalPages > 1 && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: " \u2022 " }), _jsxs(Text, { color: "gray", dimColor: true, children: ["Page ", currentPage + 1, "/", totalPages] })] }))] }), _jsx(Table, { data: currentSnapshots, keyExtractor: (snapshot) => snapshot.id, selectedIndex: selectedIndex, columns: [
|
|
106
|
+
createComponentColumn('status', 'Status', (snapshot) => _jsx(StatusBadge, { status: snapshot.status, showText: false }), { width: 2 }),
|
|
107
|
+
createTextColumn('id', 'ID', (snapshot) => showFullId ? snapshot.id : snapshot.id.slice(0, 13), { width: showFullId ? idWidth : 15, color: 'gray', dimColor: true, bold: false }),
|
|
108
|
+
createTextColumn('name', 'Name', (snapshot) => snapshot.name || '(unnamed)', { width: nameWidth }),
|
|
109
|
+
createTextColumn('devbox', 'Devbox', (snapshot) => snapshot.devbox_id || '', { width: devboxWidth, color: 'cyan', dimColor: true, bold: false, visible: showDevboxId }),
|
|
110
|
+
createTextColumn('created', 'Created', (snapshot) => snapshot.created_at ? formatTimeAgo(new Date(snapshot.created_at).getTime()) : '', { width: timeWidth, color: 'gray', dimColor: true, bold: false }),
|
|
111
|
+
] }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: "gray", dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022"] }), totalPages > 1 && (_jsxs(Text, { color: "gray", dimColor: true, children: [' ', figures.arrowLeft, figures.arrowRight, " Page \u2022"] })), _jsxs(Text, { color: "gray", dimColor: true, children: [' ', "[q] Quit"] })] })] })), error && _jsx(ErrorMessage, { message: "Failed to list snapshots", error: error })] }));
|
|
112
|
+
};
|
|
113
|
+
export async function listSnapshots(options) {
|
|
114
|
+
const executor = createExecutor(options);
|
|
115
|
+
await executor.executeList(async () => {
|
|
116
|
+
const client = executor.getClient();
|
|
117
|
+
const params = options.devbox ? { devbox_id: options.devbox } : {};
|
|
118
|
+
return executor.fetchFromIterator(client.devboxes.listDiskSnapshots(params), {
|
|
119
|
+
limit: PAGE_SIZE,
|
|
120
|
+
});
|
|
121
|
+
}, () => _jsx(ListSnapshotsUI, { devboxId: options.devbox }), PAGE_SIZE);
|
|
122
|
+
}
|