@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
|
@@ -2,7 +2,6 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { Box, Text } from 'ink';
|
|
4
4
|
import { getClient } from '../../utils/client.js';
|
|
5
|
-
import { Header } from '../../components/Header.js';
|
|
6
5
|
import { Banner } from '../../components/Banner.js';
|
|
7
6
|
import { SpinnerComponent } from '../../components/Spinner.js';
|
|
8
7
|
import { SuccessMessage } from '../../components/SuccessMessage.js';
|
|
@@ -31,7 +30,7 @@ const CreateDevboxUI = ({ name, template }) => {
|
|
|
31
30
|
};
|
|
32
31
|
create();
|
|
33
32
|
}, []);
|
|
34
|
-
return (_jsxs(_Fragment, { children: [_jsx(Banner, {}),
|
|
33
|
+
return (_jsxs(_Fragment, { children: [_jsx(Banner, {}), loading && _jsx(SpinnerComponent, { message: "Creating..." }), result && (_jsxs(_Fragment, { children: [_jsx(SuccessMessage, { message: "Devbox created!", details: `ID: ${result.id}\nStatus: ${result.status}` }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "gray", children: "Try: " }), _jsxs(Text, { color: "cyan", children: ["rln devbox exec ", result.id, " ls"] })] })] })), error && _jsx(ErrorMessage, { message: "Failed to create devbox", error: error })] }));
|
|
35
34
|
};
|
|
36
35
|
export async function createDevbox(options) {
|
|
37
36
|
const executor = createExecutor(options);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { jsx as _jsx,
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { Box, Text, useInput, useApp, useStdout } from 'ink';
|
|
4
|
+
import TextInput from 'ink-text-input';
|
|
4
5
|
import figures from 'figures';
|
|
5
6
|
import { getClient } from '../../utils/client.js';
|
|
6
7
|
import { SpinnerComponent } from '../../components/Spinner.js';
|
|
@@ -11,6 +12,8 @@ import { Table, createTextColumn } from '../../components/Table.js';
|
|
|
11
12
|
import { createExecutor } from '../../utils/CommandExecutor.js';
|
|
12
13
|
import { DevboxDetailPage } from '../../components/DevboxDetailPage.js';
|
|
13
14
|
import { DevboxCreatePage } from '../../components/DevboxCreatePage.js';
|
|
15
|
+
import { DevboxActionsMenu } from '../../components/DevboxActionsMenu.js';
|
|
16
|
+
import { ActionsPopup } from '../../components/ActionsPopup.js';
|
|
14
17
|
// Format time ago in a succinct way
|
|
15
18
|
const formatTimeAgo = (timestamp) => {
|
|
16
19
|
const seconds = Math.floor((Date.now() - timestamp) / 1000);
|
|
@@ -31,7 +34,6 @@ const formatTimeAgo = (timestamp) => {
|
|
|
31
34
|
const years = Math.floor(months / 12);
|
|
32
35
|
return `${years}y ago`;
|
|
33
36
|
};
|
|
34
|
-
const MAX_FETCH = 100;
|
|
35
37
|
const DEFAULT_PAGE_SIZE = 10;
|
|
36
38
|
const ListDevboxesUI = ({ status }) => {
|
|
37
39
|
const { exit } = useApp();
|
|
@@ -43,8 +45,17 @@ const ListDevboxesUI = ({ status }) => {
|
|
|
43
45
|
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
|
44
46
|
const [showDetails, setShowDetails] = React.useState(false);
|
|
45
47
|
const [showCreate, setShowCreate] = React.useState(false);
|
|
48
|
+
const [showActions, setShowActions] = React.useState(false);
|
|
49
|
+
const [showPopup, setShowPopup] = React.useState(false);
|
|
50
|
+
const [selectedOperation, setSelectedOperation] = React.useState(0);
|
|
46
51
|
const [refreshing, setRefreshing] = React.useState(false);
|
|
47
52
|
const [refreshIcon, setRefreshIcon] = React.useState(0);
|
|
53
|
+
const [searchMode, setSearchMode] = React.useState(false);
|
|
54
|
+
const [searchQuery, setSearchQuery] = React.useState('');
|
|
55
|
+
const [totalCount, setTotalCount] = React.useState(0);
|
|
56
|
+
const [hasMore, setHasMore] = React.useState(false);
|
|
57
|
+
const pageCache = React.useRef(new Map());
|
|
58
|
+
const lastIdCache = React.useRef(new Map());
|
|
48
59
|
// Calculate responsive dimensions
|
|
49
60
|
const terminalWidth = stdout?.columns || 120;
|
|
50
61
|
const terminalHeight = stdout?.rows || 30;
|
|
@@ -76,6 +87,18 @@ const ListDevboxesUI = ({ status }) => {
|
|
|
76
87
|
const remainingWidth = terminalWidth - fixedWidth - statusIconWidth - idWidth - statusTextWidth - timeWidth - 10;
|
|
77
88
|
nameWidth = Math.max(8, remainingWidth);
|
|
78
89
|
}
|
|
90
|
+
// Define allOperations
|
|
91
|
+
const allOperations = [
|
|
92
|
+
{ key: 'logs', label: 'View Logs', color: 'blue', icon: figures.info, shortcut: 'l' },
|
|
93
|
+
{ key: 'exec', label: 'Execute Command', color: 'green', icon: figures.play, shortcut: 'e' },
|
|
94
|
+
{ key: 'upload', label: 'Upload File', color: 'green', icon: figures.arrowUp, shortcut: 'u' },
|
|
95
|
+
{ key: 'snapshot', label: 'Create Snapshot', color: 'yellow', icon: figures.circleFilled, shortcut: 'n' },
|
|
96
|
+
{ key: 'ssh', label: 'SSH onto the box', color: 'cyan', icon: figures.arrowRight, shortcut: 's' },
|
|
97
|
+
{ key: 'tunnel', label: 'Open Tunnel', color: 'magenta', icon: figures.pointerSmall, shortcut: 't' },
|
|
98
|
+
{ key: 'suspend', label: 'Suspend Devbox', color: 'yellow', icon: figures.squareSmallFilled, shortcut: 'p' },
|
|
99
|
+
{ key: 'resume', label: 'Resume Devbox', color: 'green', icon: figures.play, shortcut: 'r' },
|
|
100
|
+
{ key: 'delete', label: 'Shutdown Devbox', color: 'red', icon: figures.cross, shortcut: 'd' },
|
|
101
|
+
];
|
|
79
102
|
React.useEffect(() => {
|
|
80
103
|
const list = async (isInitialLoad = false) => {
|
|
81
104
|
try {
|
|
@@ -83,22 +106,53 @@ const ListDevboxesUI = ({ status }) => {
|
|
|
83
106
|
if (isInitialLoad) {
|
|
84
107
|
setRefreshing(true);
|
|
85
108
|
}
|
|
109
|
+
// Check if we have cached data for this page
|
|
110
|
+
if (!isInitialLoad && pageCache.current.has(currentPage)) {
|
|
111
|
+
setDevboxes(pageCache.current.get(currentPage) || []);
|
|
112
|
+
setLoading(false);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
86
115
|
const client = getClient();
|
|
87
|
-
const
|
|
116
|
+
const pageDevboxes = [];
|
|
117
|
+
// Get starting_after cursor from previous page's last ID
|
|
118
|
+
const startingAfter = currentPage > 0 ? lastIdCache.current.get(currentPage - 1) : undefined;
|
|
119
|
+
// Build query params
|
|
120
|
+
const queryParams = {
|
|
121
|
+
limit: PAGE_SIZE,
|
|
122
|
+
};
|
|
123
|
+
if (startingAfter) {
|
|
124
|
+
queryParams.starting_after = startingAfter;
|
|
125
|
+
}
|
|
126
|
+
if (status) {
|
|
127
|
+
queryParams.status = status;
|
|
128
|
+
}
|
|
129
|
+
// Fetch only the current page
|
|
130
|
+
const page = await client.devboxes.list(queryParams);
|
|
131
|
+
// Collect items from the page - only get PAGE_SIZE items, don't auto-paginate
|
|
88
132
|
let count = 0;
|
|
89
|
-
for await (const devbox of
|
|
90
|
-
|
|
91
|
-
allDevboxes.push(devbox);
|
|
92
|
-
}
|
|
133
|
+
for await (const devbox of page) {
|
|
134
|
+
pageDevboxes.push(devbox);
|
|
93
135
|
count++;
|
|
94
|
-
|
|
136
|
+
// Break after getting PAGE_SIZE items to prevent auto-pagination
|
|
137
|
+
if (count >= PAGE_SIZE) {
|
|
95
138
|
break;
|
|
96
139
|
}
|
|
97
140
|
}
|
|
98
|
-
//
|
|
141
|
+
// Update pagination metadata from the page object
|
|
142
|
+
// These properties are on the page object itself
|
|
143
|
+
const total = page.total_count || pageDevboxes.length;
|
|
144
|
+
const more = page.has_more || false;
|
|
145
|
+
setTotalCount(total);
|
|
146
|
+
setHasMore(more);
|
|
147
|
+
// Cache the page data and last ID
|
|
148
|
+
if (pageDevboxes.length > 0) {
|
|
149
|
+
pageCache.current.set(currentPage, pageDevboxes);
|
|
150
|
+
lastIdCache.current.set(currentPage, pageDevboxes[pageDevboxes.length - 1].id);
|
|
151
|
+
}
|
|
152
|
+
// Update devboxes for current page
|
|
99
153
|
setDevboxes((prev) => {
|
|
100
|
-
const hasChanged = JSON.stringify(prev) !== JSON.stringify(
|
|
101
|
-
return hasChanged ?
|
|
154
|
+
const hasChanged = JSON.stringify(prev) !== JSON.stringify(pageDevboxes);
|
|
155
|
+
return hasChanged ? pageDevboxes : prev;
|
|
102
156
|
});
|
|
103
157
|
}
|
|
104
158
|
catch (err) {
|
|
@@ -115,24 +169,35 @@ const ListDevboxesUI = ({ status }) => {
|
|
|
115
169
|
list(true);
|
|
116
170
|
// Poll every 3 seconds (increased from 2), but only when in list view
|
|
117
171
|
const interval = setInterval(() => {
|
|
118
|
-
if (!showDetails && !showCreate) {
|
|
172
|
+
if (!showDetails && !showCreate && !showActions) {
|
|
173
|
+
// Clear cache on refresh to get latest data
|
|
174
|
+
pageCache.current.clear();
|
|
175
|
+
lastIdCache.current.clear();
|
|
119
176
|
list(false);
|
|
120
177
|
}
|
|
121
178
|
}, 3000);
|
|
122
179
|
return () => clearInterval(interval);
|
|
123
|
-
}, [showDetails, showCreate]);
|
|
180
|
+
}, [showDetails, showCreate, showActions, currentPage]);
|
|
124
181
|
// Animate refresh icon only when in list view
|
|
125
182
|
React.useEffect(() => {
|
|
126
|
-
if (showDetails || showCreate) {
|
|
183
|
+
if (showDetails || showCreate || showActions) {
|
|
127
184
|
return; // Don't animate when not in list view
|
|
128
185
|
}
|
|
129
186
|
const interval = setInterval(() => {
|
|
130
187
|
setRefreshIcon((prev) => (prev + 1) % 10);
|
|
131
188
|
}, 80);
|
|
132
189
|
return () => clearInterval(interval);
|
|
133
|
-
}, [showDetails, showCreate]);
|
|
190
|
+
}, [showDetails, showCreate, showActions]);
|
|
134
191
|
useInput((input, key) => {
|
|
135
192
|
const pageDevboxes = currentDevboxes.length;
|
|
193
|
+
// Skip input handling when in search mode - let TextInput handle it
|
|
194
|
+
if (searchMode) {
|
|
195
|
+
if (key.escape) {
|
|
196
|
+
setSearchMode(false);
|
|
197
|
+
setSearchQuery('');
|
|
198
|
+
}
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
136
201
|
// Skip input handling when in details view - let DevboxDetailPage handle it
|
|
137
202
|
if (showDetails) {
|
|
138
203
|
return;
|
|
@@ -141,6 +206,41 @@ const ListDevboxesUI = ({ status }) => {
|
|
|
141
206
|
if (showCreate) {
|
|
142
207
|
return;
|
|
143
208
|
}
|
|
209
|
+
// Skip input handling when in actions view - let DevboxActionsMenu handle it
|
|
210
|
+
if (showActions) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
// Handle popup navigation
|
|
214
|
+
if (showPopup) {
|
|
215
|
+
if (key.escape || input === 'q') {
|
|
216
|
+
console.clear();
|
|
217
|
+
setShowPopup(false);
|
|
218
|
+
setSelectedOperation(0);
|
|
219
|
+
}
|
|
220
|
+
else if (key.upArrow && selectedOperation > 0) {
|
|
221
|
+
setSelectedOperation(selectedOperation - 1);
|
|
222
|
+
}
|
|
223
|
+
else if (key.downArrow && selectedOperation < operations.length - 1) {
|
|
224
|
+
setSelectedOperation(selectedOperation + 1);
|
|
225
|
+
}
|
|
226
|
+
else if (key.return) {
|
|
227
|
+
// Execute the selected operation
|
|
228
|
+
console.clear();
|
|
229
|
+
setShowPopup(false);
|
|
230
|
+
setShowActions(true);
|
|
231
|
+
}
|
|
232
|
+
else if (input) {
|
|
233
|
+
// Check for shortcut match
|
|
234
|
+
const matchedOpIndex = operations.findIndex(op => op.shortcut === input);
|
|
235
|
+
if (matchedOpIndex !== -1) {
|
|
236
|
+
setSelectedOperation(matchedOpIndex);
|
|
237
|
+
console.clear();
|
|
238
|
+
setShowPopup(false);
|
|
239
|
+
setShowActions(true);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
144
244
|
// Handle list view
|
|
145
245
|
if (key.upArrow && selectedIndex > 0) {
|
|
146
246
|
setSelectedIndex(selectedIndex - 1);
|
|
@@ -160,6 +260,11 @@ const ListDevboxesUI = ({ status }) => {
|
|
|
160
260
|
console.clear();
|
|
161
261
|
setShowDetails(true);
|
|
162
262
|
}
|
|
263
|
+
else if (input === 'a') {
|
|
264
|
+
console.clear();
|
|
265
|
+
setShowPopup(true);
|
|
266
|
+
setSelectedOperation(0);
|
|
267
|
+
}
|
|
163
268
|
else if (input === 'c') {
|
|
164
269
|
console.clear();
|
|
165
270
|
setShowCreate(true);
|
|
@@ -184,17 +289,57 @@ const ListDevboxesUI = ({ status }) => {
|
|
|
184
289
|
};
|
|
185
290
|
openBrowser();
|
|
186
291
|
}
|
|
292
|
+
else if (input === '/') {
|
|
293
|
+
setSearchMode(true);
|
|
294
|
+
}
|
|
295
|
+
else if (key.escape && searchQuery) {
|
|
296
|
+
// Clear search when Esc is pressed and there's an active search
|
|
297
|
+
setSearchQuery('');
|
|
298
|
+
setCurrentPage(0);
|
|
299
|
+
setSelectedIndex(0);
|
|
300
|
+
}
|
|
187
301
|
else if (input === 'q') {
|
|
188
302
|
process.exit(0);
|
|
189
303
|
}
|
|
190
304
|
});
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
305
|
+
// Filter devboxes based on search query (client-side only for current page)
|
|
306
|
+
const filteredDevboxes = React.useMemo(() => {
|
|
307
|
+
if (!searchQuery.trim())
|
|
308
|
+
return devboxes;
|
|
309
|
+
const query = searchQuery.toLowerCase();
|
|
310
|
+
return devboxes.filter(devbox => {
|
|
311
|
+
return (devbox.id?.toLowerCase().includes(query) ||
|
|
312
|
+
devbox.name?.toLowerCase().includes(query) ||
|
|
313
|
+
devbox.status?.toLowerCase().includes(query));
|
|
314
|
+
});
|
|
315
|
+
}, [devboxes, searchQuery]);
|
|
316
|
+
const running = filteredDevboxes.filter((d) => d.status === 'running').length;
|
|
317
|
+
const stopped = filteredDevboxes.filter((d) => ['stopped', 'suspended'].includes(d.status)).length;
|
|
318
|
+
// Current page is already fetched, no need to slice
|
|
319
|
+
const currentDevboxes = filteredDevboxes;
|
|
197
320
|
const selectedDevbox = currentDevboxes[selectedIndex];
|
|
321
|
+
// Calculate pagination info
|
|
322
|
+
const totalPages = Math.ceil(totalCount / PAGE_SIZE);
|
|
323
|
+
const startIndex = currentPage * PAGE_SIZE;
|
|
324
|
+
const endIndex = startIndex + currentDevboxes.length;
|
|
325
|
+
// Filter operations based on devbox status
|
|
326
|
+
const operations = selectedDevbox ? allOperations.filter(op => {
|
|
327
|
+
const status = selectedDevbox.status;
|
|
328
|
+
// When suspended: logs and resume
|
|
329
|
+
if (status === 'suspended') {
|
|
330
|
+
return op.key === 'resume' || op.key === 'logs';
|
|
331
|
+
}
|
|
332
|
+
// When not running (shutdown, failure, etc): only logs
|
|
333
|
+
if (status !== 'running' && status !== 'provisioning' && status !== 'initializing') {
|
|
334
|
+
return op.key === 'logs';
|
|
335
|
+
}
|
|
336
|
+
// When running: everything except resume
|
|
337
|
+
if (status === 'running') {
|
|
338
|
+
return op.key !== 'resume';
|
|
339
|
+
}
|
|
340
|
+
// Default for transitional states (provisioning, initializing)
|
|
341
|
+
return op.key === 'logs' || op.key === 'delete';
|
|
342
|
+
}) : allOperations;
|
|
198
343
|
// Create view
|
|
199
344
|
if (showCreate) {
|
|
200
345
|
return (_jsx(DevboxCreatePage, { onBack: () => {
|
|
@@ -205,32 +350,82 @@ const ListDevboxesUI = ({ status }) => {
|
|
|
205
350
|
// The list will auto-refresh via the polling effect
|
|
206
351
|
} }));
|
|
207
352
|
}
|
|
353
|
+
// Actions view
|
|
354
|
+
if (showActions && selectedDevbox) {
|
|
355
|
+
const selectedOp = operations[selectedOperation];
|
|
356
|
+
return (_jsx(DevboxActionsMenu, { devbox: selectedDevbox, onBack: () => {
|
|
357
|
+
setShowActions(false);
|
|
358
|
+
setSelectedOperation(0);
|
|
359
|
+
}, breadcrumbItems: [
|
|
360
|
+
{ label: 'Devboxes' },
|
|
361
|
+
{ label: selectedDevbox.name || selectedDevbox.id, active: true }
|
|
362
|
+
], initialOperation: selectedOp?.key, initialOperationIndex: selectedOperation }));
|
|
363
|
+
}
|
|
208
364
|
// Details view
|
|
209
365
|
if (showDetails && selectedDevbox) {
|
|
210
366
|
return _jsx(DevboxDetailPage, { devbox: selectedDevbox, onBack: () => setShowDetails(false) });
|
|
211
367
|
}
|
|
368
|
+
// Show popup with table in background
|
|
369
|
+
if (showPopup && selectedDevbox) {
|
|
370
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
371
|
+
{ label: 'Devboxes', active: true }
|
|
372
|
+
] }), !loading && !error && devboxes.length > 0 && (_jsx(_Fragment, { children: _jsx(Table, { data: currentDevboxes, keyExtractor: (devbox) => devbox.id, selectedIndex: selectedIndex, title: `devboxes[${totalCount}]`, columns: [
|
|
373
|
+
{
|
|
374
|
+
key: 'statusIcon',
|
|
375
|
+
label: '',
|
|
376
|
+
width: statusIconWidth,
|
|
377
|
+
render: (devbox, index, isSelected) => {
|
|
378
|
+
const statusDisplay = getStatusDisplay(devbox.status);
|
|
379
|
+
// Truncate icon to fit width and pad
|
|
380
|
+
const icon = statusDisplay.icon.slice(0, statusIconWidth);
|
|
381
|
+
const padded = icon.padEnd(statusIconWidth, ' ');
|
|
382
|
+
return (_jsx(Text, { color: isSelected ? 'white' : statusDisplay.color, bold: true, inverse: isSelected, children: padded }));
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
createTextColumn('id', 'ID', (devbox) => devbox.id, { width: idWidth, color: 'gray', dimColor: true, bold: false }),
|
|
386
|
+
{
|
|
387
|
+
key: 'statusText',
|
|
388
|
+
label: 'Status',
|
|
389
|
+
width: statusTextWidth,
|
|
390
|
+
render: (devbox, index, isSelected) => {
|
|
391
|
+
const statusDisplay = getStatusDisplay(devbox.status);
|
|
392
|
+
const truncated = statusDisplay.text.slice(0, statusTextWidth);
|
|
393
|
+
const padded = truncated.padEnd(statusTextWidth, ' ');
|
|
394
|
+
return (_jsx(Text, { color: isSelected ? 'white' : statusDisplay.color, bold: true, inverse: isSelected, children: padded }));
|
|
395
|
+
}
|
|
396
|
+
},
|
|
397
|
+
createTextColumn('name', 'Name', (devbox) => devbox.name || '', { width: nameWidth, dimColor: true }),
|
|
398
|
+
createTextColumn('capabilities', 'Capabilities', (devbox) => {
|
|
399
|
+
const hasCapabilities = devbox.capabilities && devbox.capabilities.filter((c) => c !== 'unknown').length > 0;
|
|
400
|
+
return hasCapabilities
|
|
401
|
+
? `[${devbox.capabilities
|
|
402
|
+
.filter((c) => c !== 'unknown')
|
|
403
|
+
.map((c) => c === 'computer_usage' ? 'comp' : c === 'browser_usage' ? 'browser' : c === 'docker_in_docker' ? 'docker' : c)
|
|
404
|
+
.join(',')}]`
|
|
405
|
+
: '';
|
|
406
|
+
}, { width: capabilitiesWidth, color: 'blue', dimColor: true, bold: false, visible: showCapabilities }),
|
|
407
|
+
createTextColumn('tags', 'Tags', (devbox) => devbox.blueprint_id ? '[bp]' : devbox.snapshot_id ? '[snap]' : '', { width: tagWidth, color: 'yellow', dimColor: true, bold: false, visible: showTags }),
|
|
408
|
+
createTextColumn('created', 'Created', (devbox) => devbox.create_time_ms ? formatTimeAgo(devbox.create_time_ms) : '', { width: timeWidth, color: 'gray', dimColor: true, bold: false }),
|
|
409
|
+
] }) })), _jsx(Box, { marginTop: -Math.min(operations.length + 10, PAGE_SIZE + 5), justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedDevbox, operations: operations, selectedOperation: selectedOperation, onClose: () => setShowPopup(false) }) })] }));
|
|
410
|
+
}
|
|
212
411
|
// List view
|
|
213
412
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
214
413
|
{ 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: [
|
|
414
|
+
] }), 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: [searchMode && (_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: "cyan", children: [figures.pointerSmall, " Search: "] }), _jsx(TextInput, { value: searchQuery, onChange: setSearchQuery, placeholder: "Type to search (name, id, status)...", onSubmit: () => {
|
|
415
|
+
setSearchMode(false);
|
|
416
|
+
setCurrentPage(0);
|
|
417
|
+
setSelectedIndex(0);
|
|
418
|
+
} }), _jsx(Text, { color: "gray", dimColor: true, children: " [Esc to cancel]" })] })), searchQuery && !searchMode && (_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: "cyan", children: [figures.info, " Searching for: "] }), _jsx(Text, { color: "yellow", bold: true, children: searchQuery }), _jsxs(Text, { color: "gray", dimColor: true, children: [" (", filteredDevboxes.length, " results) [/ to edit, Esc to clear]"] })] })), _jsx(Table, { data: currentDevboxes, keyExtractor: (devbox) => devbox.id, selectedIndex: selectedIndex, title: `devboxes[${totalCount}]`, columns: [
|
|
216
419
|
{
|
|
217
420
|
key: 'statusIcon',
|
|
218
421
|
label: '',
|
|
219
422
|
width: statusIconWidth,
|
|
220
423
|
render: (devbox, index, isSelected) => {
|
|
221
424
|
const statusDisplay = getStatusDisplay(devbox.status);
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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 }));
|
|
425
|
+
// Truncate icon to fit width and pad
|
|
426
|
+
const icon = statusDisplay.icon.slice(0, statusIconWidth);
|
|
427
|
+
const padded = icon.padEnd(statusIconWidth, ' ');
|
|
428
|
+
return (_jsx(Text, { color: isSelected ? 'white' : statusDisplay.color, bold: true, inverse: isSelected, children: padded }));
|
|
234
429
|
}
|
|
235
430
|
},
|
|
236
431
|
createTextColumn('id', 'ID', (devbox) => devbox.id, { width: idWidth, color: 'gray', dimColor: true, bold: false }),
|
|
@@ -240,19 +435,9 @@ const ListDevboxesUI = ({ status }) => {
|
|
|
240
435
|
width: statusTextWidth,
|
|
241
436
|
render: (devbox, index, isSelected) => {
|
|
242
437
|
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
438
|
const truncated = statusDisplay.text.slice(0, statusTextWidth);
|
|
254
439
|
const padded = truncated.padEnd(statusTextWidth, ' ');
|
|
255
|
-
return (_jsx(Text, { color: isSelected ? 'white' : color, bold: true, inverse: isSelected, children: padded }));
|
|
440
|
+
return (_jsx(Text, { color: isSelected ? 'white' : statusDisplay.color, bold: true, inverse: isSelected, children: padded }));
|
|
256
441
|
}
|
|
257
442
|
},
|
|
258
443
|
createTextColumn('name', 'Name', (devbox) => devbox.name || '', { width: nameWidth }),
|
|
@@ -267,7 +452,7 @@ const ListDevboxesUI = ({ status }) => {
|
|
|
267
452
|
}, { width: capabilitiesWidth, color: 'blue', dimColor: true, bold: false, visible: showCapabilities }),
|
|
268
453
|
createTextColumn('tags', 'Tags', (devbox) => devbox.blueprint_id ? '[bp]' : devbox.snapshot_id ? '[snap]' : '', { width: tagWidth, color: 'yellow', dimColor: true, bold: false, visible: showTags }),
|
|
269
454
|
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, " ",
|
|
455
|
+
] }), _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" }), 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 ", totalCount] }), hasMore && (_jsx(Text, { color: "gray", dimColor: true, children: " (more available)" })), _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] Details \u2022 [a] Actions \u2022 [c] Create \u2022 [/] Search \u2022 [o] Browser \u2022 [q] Quit"] })] })] })), error && _jsx(ErrorMessage, { message: "Failed to list devboxes", error: error })] }));
|
|
271
456
|
};
|
|
272
457
|
export async function listDevboxes(options) {
|
|
273
458
|
const executor = createExecutor(options);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import figures from 'figures';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
export const ActionsPopup = ({ devbox, operations, selectedOperation, onClose, }) => {
|
|
6
|
+
// Calculate the maximum width needed
|
|
7
|
+
const maxLabelLength = Math.max(...operations.map(op => op.label.length));
|
|
8
|
+
const contentWidth = maxLabelLength + 12; // Content + icon + pointer + shortcuts
|
|
9
|
+
// Strip ANSI codes to get real length, then pad
|
|
10
|
+
const stripAnsi = (str) => str.replace(/\u001b\[[0-9;]*m/g, '');
|
|
11
|
+
const bgLine = (content) => {
|
|
12
|
+
const cleanLength = stripAnsi(content).length;
|
|
13
|
+
const padding = Math.max(0, contentWidth - cleanLength);
|
|
14
|
+
return chalk.bgBlack(content + ' '.repeat(padding));
|
|
15
|
+
};
|
|
16
|
+
// Render all lines with background
|
|
17
|
+
const lines = [
|
|
18
|
+
bgLine(chalk.cyan.bold(` ${figures.play} Quick Actions`)),
|
|
19
|
+
chalk.bgBlack(' '.repeat(contentWidth)),
|
|
20
|
+
...operations.map((op, index) => {
|
|
21
|
+
const isSelected = index === selectedOperation;
|
|
22
|
+
const pointer = isSelected ? figures.pointer : ' ';
|
|
23
|
+
const content = ` ${pointer} ${op.icon} ${op.label} [${op.shortcut}]`;
|
|
24
|
+
let styled;
|
|
25
|
+
if (isSelected) {
|
|
26
|
+
const colorFn = chalk[op.color];
|
|
27
|
+
styled = typeof colorFn === 'function' ? colorFn.bold(content) : chalk.white.bold(content);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
styled = chalk.gray(content);
|
|
31
|
+
}
|
|
32
|
+
return bgLine(styled);
|
|
33
|
+
}),
|
|
34
|
+
chalk.bgBlack(' '.repeat(contentWidth)),
|
|
35
|
+
bgLine(chalk.gray.dim(` ${figures.arrowUp}${figures.arrowDown} Nav • [Enter]`)),
|
|
36
|
+
bgLine(chalk.gray.dim(` [Esc] Close`)),
|
|
37
|
+
];
|
|
38
|
+
// Draw custom border with background to fill gaps
|
|
39
|
+
const borderTop = chalk.cyan('╭' + '─'.repeat(contentWidth) + '╮');
|
|
40
|
+
const borderBottom = chalk.cyan('╰' + '─'.repeat(contentWidth) + '╯');
|
|
41
|
+
return (_jsx(Box, { flexDirection: "column", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: borderTop }), lines.map((line, i) => (_jsxs(Text, { children: [chalk.cyan('│'), line, chalk.cyan('│')] }, i))), _jsx(Text, { children: borderBottom })] }) }));
|
|
42
|
+
};
|