@runloop/rl-cli 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -0
- package/dist/cli.js +73 -60
- package/dist/commands/auth.js +0 -1
- package/dist/commands/blueprint/create.js +31 -83
- package/dist/commands/blueprint/get.js +29 -34
- package/dist/commands/blueprint/list.js +215 -213
- package/dist/commands/blueprint/logs.js +133 -37
- package/dist/commands/blueprint/preview.js +42 -38
- package/dist/commands/config.js +117 -0
- package/dist/commands/devbox/create.js +120 -40
- package/dist/commands/devbox/delete.js +17 -33
- package/dist/commands/devbox/download.js +29 -43
- package/dist/commands/devbox/exec.js +22 -39
- package/dist/commands/devbox/execAsync.js +20 -37
- package/dist/commands/devbox/get.js +13 -35
- package/dist/commands/devbox/getAsync.js +12 -34
- package/dist/commands/devbox/list.js +241 -402
- package/dist/commands/devbox/logs.js +20 -38
- package/dist/commands/devbox/read.js +29 -43
- package/dist/commands/devbox/resume.js +13 -35
- package/dist/commands/devbox/rsync.js +26 -78
- package/dist/commands/devbox/scp.js +25 -79
- package/dist/commands/devbox/sendStdin.js +41 -0
- package/dist/commands/devbox/shutdown.js +13 -35
- package/dist/commands/devbox/ssh.js +45 -78
- package/dist/commands/devbox/suspend.js +13 -35
- package/dist/commands/devbox/tunnel.js +36 -88
- package/dist/commands/devbox/upload.js +28 -36
- package/dist/commands/devbox/write.js +29 -44
- package/dist/commands/mcp-install.js +4 -3
- package/dist/commands/menu.js +24 -66
- package/dist/commands/object/delete.js +12 -34
- package/dist/commands/object/download.js +26 -74
- package/dist/commands/object/get.js +12 -34
- package/dist/commands/object/list.js +15 -93
- package/dist/commands/object/upload.js +35 -96
- package/dist/commands/snapshot/create.js +23 -39
- package/dist/commands/snapshot/delete.js +17 -33
- package/dist/commands/snapshot/get.js +16 -0
- package/dist/commands/snapshot/list.js +309 -80
- package/dist/commands/snapshot/status.js +12 -34
- package/dist/components/ActionsPopup.js +63 -39
- package/dist/components/Breadcrumb.js +10 -48
- package/dist/components/DevboxActionsMenu.js +182 -110
- package/dist/components/DevboxCreatePage.js +12 -7
- package/dist/components/DevboxDetailPage.js +76 -28
- package/dist/components/ErrorBoundary.js +29 -0
- package/dist/components/ErrorMessage.js +10 -2
- package/dist/components/Header.js +12 -4
- package/dist/components/InteractiveSpawn.js +94 -0
- package/dist/components/MainMenu.js +36 -32
- package/dist/components/MetadataDisplay.js +4 -4
- package/dist/components/OperationsMenu.js +1 -1
- package/dist/components/ResourceActionsMenu.js +4 -4
- package/dist/components/ResourceListView.js +46 -34
- package/dist/components/Spinner.js +7 -2
- package/dist/components/StatusBadge.js +1 -1
- package/dist/components/SuccessMessage.js +12 -2
- package/dist/components/Table.js +16 -6
- package/dist/hooks/useCursorPagination.js +125 -85
- package/dist/hooks/useExitOnCtrlC.js +14 -0
- package/dist/hooks/useViewportHeight.js +47 -0
- package/dist/mcp/server.js +65 -6
- package/dist/router/Router.js +68 -0
- package/dist/router/types.js +1 -0
- package/dist/screens/BlueprintListScreen.js +7 -0
- package/dist/screens/DevboxActionsScreen.js +25 -0
- package/dist/screens/DevboxCreateScreen.js +11 -0
- package/dist/screens/DevboxDetailScreen.js +60 -0
- package/dist/screens/DevboxListScreen.js +23 -0
- package/dist/screens/LogsSessionScreen.js +49 -0
- package/dist/screens/MenuScreen.js +23 -0
- package/dist/screens/SSHSessionScreen.js +55 -0
- package/dist/screens/SnapshotListScreen.js +7 -0
- package/dist/services/blueprintService.js +105 -0
- package/dist/services/devboxService.js +215 -0
- package/dist/services/snapshotService.js +81 -0
- package/dist/store/blueprintStore.js +89 -0
- package/dist/store/devboxStore.js +105 -0
- package/dist/store/index.js +7 -0
- package/dist/store/navigationStore.js +101 -0
- package/dist/store/snapshotStore.js +87 -0
- package/dist/utils/CommandExecutor.js +53 -24
- package/dist/utils/client.js +0 -2
- package/dist/utils/config.js +22 -111
- package/dist/utils/interactiveCommand.js +3 -2
- package/dist/utils/logFormatter.js +162 -0
- package/dist/utils/memoryMonitor.js +85 -0
- package/dist/utils/output.js +150 -59
- package/dist/utils/screen.js +23 -0
- package/dist/utils/ssh.js +3 -1
- package/dist/utils/sshSession.js +5 -29
- package/dist/utils/terminalDetection.js +97 -0
- package/dist/utils/terminalSync.js +39 -0
- package/dist/utils/theme.js +147 -13
- package/package.json +16 -13
|
@@ -1,6 +1,6 @@
|
|
|
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
|
-
import { Box, Text, useInput, useApp
|
|
3
|
+
import { Box, Text, useInput, useApp } from "ink";
|
|
4
4
|
import TextInput from "ink-text-input";
|
|
5
5
|
import figures from "figures";
|
|
6
6
|
import { getClient } from "../../utils/client.js";
|
|
@@ -10,43 +10,92 @@ import { getStatusDisplay } from "../../components/StatusBadge.js";
|
|
|
10
10
|
import { Breadcrumb } from "../../components/Breadcrumb.js";
|
|
11
11
|
import { Table, createTextColumn } from "../../components/Table.js";
|
|
12
12
|
import { formatTimeAgo } from "../../components/ResourceListView.js";
|
|
13
|
-
import {
|
|
13
|
+
import { output, outputError } from "../../utils/output.js";
|
|
14
14
|
import { DevboxDetailPage } from "../../components/DevboxDetailPage.js";
|
|
15
15
|
import { DevboxCreatePage } from "../../components/DevboxCreatePage.js";
|
|
16
16
|
import { ResourceActionsMenu } from "../../components/ResourceActionsMenu.js";
|
|
17
17
|
import { ActionsPopup } from "../../components/ActionsPopup.js";
|
|
18
18
|
import { getDevboxUrl } from "../../utils/url.js";
|
|
19
|
-
import {
|
|
19
|
+
import { useViewportHeight } from "../../hooks/useViewportHeight.js";
|
|
20
|
+
import { useExitOnCtrlC } from "../../hooks/useExitOnCtrlC.js";
|
|
21
|
+
import { useCursorPagination } from "../../hooks/useCursorPagination.js";
|
|
20
22
|
import { colors } from "../../utils/theme.js";
|
|
23
|
+
import { useDevboxStore } from "../../store/devboxStore.js";
|
|
21
24
|
const DEFAULT_PAGE_SIZE = 10;
|
|
22
|
-
const ListDevboxesUI = ({ status,
|
|
25
|
+
const ListDevboxesUI = ({ status, onBack, onExit, onNavigateToDetail, }) => {
|
|
23
26
|
const { exit: inkExit } = useApp();
|
|
24
|
-
const { stdout } = useStdout();
|
|
25
|
-
const [initialLoading, setInitialLoading] = React.useState(true);
|
|
26
|
-
const [devboxes, setDevboxes] = React.useState([]);
|
|
27
|
-
const [error, setError] = React.useState(null);
|
|
28
|
-
const [currentPage, setCurrentPage] = React.useState(0);
|
|
29
27
|
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
|
30
28
|
const [showDetails, setShowDetails] = React.useState(false);
|
|
31
29
|
const [showCreate, setShowCreate] = React.useState(false);
|
|
32
30
|
const [showActions, setShowActions] = React.useState(false);
|
|
33
31
|
const [showPopup, setShowPopup] = React.useState(false);
|
|
34
32
|
const [selectedOperation, setSelectedOperation] = React.useState(0);
|
|
35
|
-
const [refreshing, setRefreshing] = React.useState(false);
|
|
36
|
-
const [refreshIcon, setRefreshIcon] = React.useState(0);
|
|
37
|
-
const isNavigating = React.useRef(false);
|
|
38
33
|
const [searchMode, setSearchMode] = React.useState(false);
|
|
39
34
|
const [searchQuery, setSearchQuery] = React.useState("");
|
|
40
|
-
const [
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
|
|
35
|
+
const [submittedSearchQuery, setSubmittedSearchQuery] = React.useState("");
|
|
36
|
+
// Get devbox store setter to sync data for detail screen
|
|
37
|
+
const setDevboxesInStore = useDevboxStore((state) => state.setDevboxes);
|
|
38
|
+
// Calculate overhead for viewport height:
|
|
39
|
+
// - Breadcrumb (3 lines + marginBottom): 4 lines
|
|
40
|
+
// - Search bar (if visible, 1 line + marginBottom): 2 lines
|
|
41
|
+
// - Table (title + top border + header + bottom border): 4 lines
|
|
42
|
+
// - Stats bar (marginTop + content): 2 lines
|
|
43
|
+
// - Help bar (marginTop + content): 2 lines
|
|
44
|
+
// - Safety buffer for edge cases: 1 line
|
|
45
|
+
// Total: 13 lines base + 2 if searching
|
|
46
|
+
const overhead = 13 + (searchMode || submittedSearchQuery ? 2 : 0);
|
|
47
|
+
const { viewportHeight, terminalWidth } = useViewportHeight({
|
|
48
|
+
overhead,
|
|
49
|
+
minHeight: 5,
|
|
50
|
+
});
|
|
51
|
+
const PAGE_SIZE = viewportHeight;
|
|
52
|
+
// Fetch function for pagination hook
|
|
53
|
+
const fetchPage = React.useCallback(async (params) => {
|
|
54
|
+
const client = getClient();
|
|
55
|
+
const pageDevboxes = [];
|
|
56
|
+
// Build query params
|
|
57
|
+
const queryParams = {
|
|
58
|
+
limit: params.limit,
|
|
59
|
+
};
|
|
60
|
+
if (params.startingAt) {
|
|
61
|
+
queryParams.starting_after = params.startingAt;
|
|
62
|
+
}
|
|
63
|
+
if (status) {
|
|
64
|
+
queryParams.status = status;
|
|
65
|
+
}
|
|
66
|
+
if (submittedSearchQuery) {
|
|
67
|
+
queryParams.search = submittedSearchQuery;
|
|
68
|
+
}
|
|
69
|
+
// Fetch ONE page only
|
|
70
|
+
const page = (await client.devboxes.list(queryParams));
|
|
71
|
+
// Extract data and create defensive copies using JSON serialization
|
|
72
|
+
if (page.devboxes && Array.isArray(page.devboxes)) {
|
|
73
|
+
page.devboxes.forEach((d) => {
|
|
74
|
+
pageDevboxes.push(JSON.parse(JSON.stringify(d)));
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
const result = {
|
|
78
|
+
items: pageDevboxes,
|
|
79
|
+
hasMore: page.has_more || false,
|
|
80
|
+
totalCount: page.total_count || pageDevboxes.length,
|
|
81
|
+
};
|
|
82
|
+
return result;
|
|
83
|
+
}, [status, submittedSearchQuery]);
|
|
84
|
+
// Use the shared pagination hook
|
|
85
|
+
const { items: devboxes, loading, navigating, error, currentPage, hasMore, hasPrev, totalCount, nextPage, prevPage, } = useCursorPagination({
|
|
86
|
+
fetchPage,
|
|
87
|
+
pageSize: PAGE_SIZE,
|
|
88
|
+
getItemId: (devbox) => devbox.id,
|
|
89
|
+
pollInterval: 2000,
|
|
90
|
+
pollingEnabled: !showDetails && !showCreate && !showActions && !showPopup && !searchMode,
|
|
91
|
+
deps: [status, submittedSearchQuery, PAGE_SIZE],
|
|
92
|
+
});
|
|
93
|
+
// Sync devboxes to store for detail screen
|
|
94
|
+
React.useEffect(() => {
|
|
95
|
+
if (devboxes.length > 0) {
|
|
96
|
+
setDevboxesInStore(devboxes);
|
|
97
|
+
}
|
|
98
|
+
}, [devboxes, setDevboxesInStore]);
|
|
50
99
|
const fixedWidth = 4; // pointer + spaces
|
|
51
100
|
const statusIconWidth = 2;
|
|
52
101
|
const statusTextWidth = 10;
|
|
@@ -55,9 +104,11 @@ const ListDevboxesUI = ({ status, onSSHRequest, focusDevboxId, onBack, onExit })
|
|
|
55
104
|
const sourceWidth = 26;
|
|
56
105
|
// ID is always full width (25 chars for dbx_31CYd5LLFbBxst8mqnUjO format)
|
|
57
106
|
const idWidth = 26;
|
|
58
|
-
// Responsive layout based on terminal width
|
|
107
|
+
// Responsive layout based on terminal width (simplified like blueprint list)
|
|
59
108
|
const showCapabilities = terminalWidth >= 140;
|
|
60
109
|
const showSource = terminalWidth >= 120;
|
|
110
|
+
// CRITICAL: Absolute maximum column widths to prevent Yoga crashes
|
|
111
|
+
const ABSOLUTE_MAX_NAME_WIDTH = 80;
|
|
61
112
|
// Name width is flexible and uses remaining space
|
|
62
113
|
let nameWidth = 15;
|
|
63
114
|
if (terminalWidth >= 120) {
|
|
@@ -70,7 +121,7 @@ const ListDevboxesUI = ({ status, onSSHRequest, focusDevboxId, onBack, onExit })
|
|
|
70
121
|
capabilitiesWidth -
|
|
71
122
|
sourceWidth -
|
|
72
123
|
12;
|
|
73
|
-
nameWidth = Math.max(15, remainingWidth);
|
|
124
|
+
nameWidth = Math.min(ABSOLUTE_MAX_NAME_WIDTH, Math.max(15, remainingWidth));
|
|
74
125
|
}
|
|
75
126
|
else if (terminalWidth >= 110) {
|
|
76
127
|
const remainingWidth = terminalWidth -
|
|
@@ -81,7 +132,7 @@ const ListDevboxesUI = ({ status, onSSHRequest, focusDevboxId, onBack, onExit })
|
|
|
81
132
|
timeWidth -
|
|
82
133
|
sourceWidth -
|
|
83
134
|
10;
|
|
84
|
-
nameWidth = Math.max(12, remainingWidth);
|
|
135
|
+
nameWidth = Math.min(ABSOLUTE_MAX_NAME_WIDTH, Math.max(12, remainingWidth));
|
|
85
136
|
}
|
|
86
137
|
else {
|
|
87
138
|
const remainingWidth = terminalWidth -
|
|
@@ -91,10 +142,92 @@ const ListDevboxesUI = ({ status, onSSHRequest, focusDevboxId, onBack, onExit })
|
|
|
91
142
|
statusTextWidth -
|
|
92
143
|
timeWidth -
|
|
93
144
|
10;
|
|
94
|
-
nameWidth = Math.max(8, remainingWidth);
|
|
145
|
+
nameWidth = Math.min(ABSOLUTE_MAX_NAME_WIDTH, Math.max(8, remainingWidth));
|
|
95
146
|
}
|
|
96
|
-
//
|
|
97
|
-
const
|
|
147
|
+
// Build responsive column list (memoized to prevent recreating on every render)
|
|
148
|
+
const tableColumns = React.useMemo(() => {
|
|
149
|
+
const ABSOLUTE_MAX_NAME = 80;
|
|
150
|
+
const ABSOLUTE_MAX_ID = 50;
|
|
151
|
+
const columns = [
|
|
152
|
+
createTextColumn("name", "Name", (devbox) => {
|
|
153
|
+
const name = String(devbox?.name || devbox?.id || "");
|
|
154
|
+
const safeMax = Math.min(nameWidth || 15, ABSOLUTE_MAX_NAME);
|
|
155
|
+
return name.length > safeMax
|
|
156
|
+
? name.substring(0, Math.max(1, safeMax - 3)) + "..."
|
|
157
|
+
: name;
|
|
158
|
+
}, {
|
|
159
|
+
width: Math.min(nameWidth || 15, ABSOLUTE_MAX_NAME),
|
|
160
|
+
dimColor: false,
|
|
161
|
+
}),
|
|
162
|
+
createTextColumn("id", "ID", (devbox) => {
|
|
163
|
+
const id = String(devbox?.id || "");
|
|
164
|
+
const safeMax = Math.min(idWidth || 26, ABSOLUTE_MAX_ID);
|
|
165
|
+
return id.length > safeMax
|
|
166
|
+
? id.substring(0, Math.max(1, safeMax - 3)) + "..."
|
|
167
|
+
: id;
|
|
168
|
+
}, {
|
|
169
|
+
width: Math.min(idWidth || 26, ABSOLUTE_MAX_ID),
|
|
170
|
+
color: colors.idColor,
|
|
171
|
+
dimColor: false,
|
|
172
|
+
bold: false,
|
|
173
|
+
}),
|
|
174
|
+
createTextColumn("status", "Status", (devbox) => {
|
|
175
|
+
const statusDisplay = getStatusDisplay(devbox?.status);
|
|
176
|
+
const text = String(statusDisplay?.text || "-");
|
|
177
|
+
return text.length > 20 ? text.substring(0, 17) + "..." : text;
|
|
178
|
+
}, {
|
|
179
|
+
width: statusTextWidth,
|
|
180
|
+
dimColor: false,
|
|
181
|
+
}),
|
|
182
|
+
createTextColumn("created", "Created", (devbox) => {
|
|
183
|
+
const time = formatTimeAgo(devbox?.create_time_ms || Date.now());
|
|
184
|
+
const text = String(time || "-");
|
|
185
|
+
return text.length > 25 ? text.substring(0, 22) + "..." : text;
|
|
186
|
+
}, {
|
|
187
|
+
width: timeWidth,
|
|
188
|
+
color: colors.textDim,
|
|
189
|
+
dimColor: false,
|
|
190
|
+
}),
|
|
191
|
+
];
|
|
192
|
+
if (showSource) {
|
|
193
|
+
columns.push(createTextColumn("source", "Source", (devbox) => {
|
|
194
|
+
if (devbox?.blueprint_id) {
|
|
195
|
+
const bpId = String(devbox.blueprint_id);
|
|
196
|
+
const truncated = bpId.slice(0, 16);
|
|
197
|
+
const text = `${truncated}`;
|
|
198
|
+
return text.length > 30 ? text.substring(0, 27) + "..." : text;
|
|
199
|
+
}
|
|
200
|
+
return "-";
|
|
201
|
+
}, {
|
|
202
|
+
width: sourceWidth,
|
|
203
|
+
color: colors.textDim,
|
|
204
|
+
dimColor: false,
|
|
205
|
+
}));
|
|
206
|
+
}
|
|
207
|
+
if (showCapabilities) {
|
|
208
|
+
columns.push(createTextColumn("capabilities", "Capabilities", (devbox) => {
|
|
209
|
+
const caps = devbox?.capabilities || [];
|
|
210
|
+
const text = caps.length > 0 ? caps.join(",") : "-";
|
|
211
|
+
return text.length > 20 ? text.substring(0, 17) + "..." : text;
|
|
212
|
+
}, {
|
|
213
|
+
width: capabilitiesWidth,
|
|
214
|
+
color: colors.textDim,
|
|
215
|
+
dimColor: false,
|
|
216
|
+
}));
|
|
217
|
+
}
|
|
218
|
+
return columns;
|
|
219
|
+
}, [
|
|
220
|
+
nameWidth,
|
|
221
|
+
idWidth,
|
|
222
|
+
statusTextWidth,
|
|
223
|
+
timeWidth,
|
|
224
|
+
showSource,
|
|
225
|
+
sourceWidth,
|
|
226
|
+
showCapabilities,
|
|
227
|
+
capabilitiesWidth,
|
|
228
|
+
]);
|
|
229
|
+
// Define allOperations (memoized to prevent recreating on every render)
|
|
230
|
+
const allOperations = React.useMemo(() => [
|
|
98
231
|
{
|
|
99
232
|
key: "logs",
|
|
100
233
|
label: "View Logs",
|
|
@@ -158,137 +291,40 @@ const ListDevboxesUI = ({ status, onSSHRequest, focusDevboxId, onBack, onExit })
|
|
|
158
291
|
icon: figures.cross,
|
|
159
292
|
shortcut: "d",
|
|
160
293
|
},
|
|
161
|
-
];
|
|
162
|
-
//
|
|
294
|
+
], []);
|
|
295
|
+
// Handle Ctrl+C to exit
|
|
296
|
+
useExitOnCtrlC();
|
|
297
|
+
// Ensure selected index is within bounds
|
|
163
298
|
React.useEffect(() => {
|
|
164
|
-
if (
|
|
165
|
-
|
|
166
|
-
const devboxIndex = devboxes.findIndex((d) => d.id === focusDevboxId);
|
|
167
|
-
if (devboxIndex !== -1) {
|
|
168
|
-
setSelectedIndex(devboxIndex);
|
|
169
|
-
setShowDetails(true);
|
|
170
|
-
}
|
|
299
|
+
if (devboxes.length > 0 && selectedIndex >= devboxes.length) {
|
|
300
|
+
setSelectedIndex(Math.max(0, devboxes.length - 1));
|
|
171
301
|
}
|
|
172
|
-
}, [devboxes,
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
isNavigating.current = true;
|
|
185
|
-
}
|
|
186
|
-
// Only show refreshing indicator on initial load
|
|
187
|
-
if (isInitialLoad) {
|
|
188
|
-
setRefreshing(true);
|
|
189
|
-
}
|
|
190
|
-
// Check if we have cached data for this page
|
|
191
|
-
if (!isInitialLoad &&
|
|
192
|
-
!isBackgroundRefresh &&
|
|
193
|
-
pageCache.current.has(currentPage)) {
|
|
194
|
-
setDevboxes(pageCache.current.get(currentPage) || []);
|
|
195
|
-
isNavigating.current = false;
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
const client = getClient();
|
|
199
|
-
const pageDevboxes = [];
|
|
200
|
-
// Get starting_after cursor from previous page's last ID
|
|
201
|
-
const startingAfter = currentPage > 0
|
|
202
|
-
? lastIdCache.current.get(currentPage - 1)
|
|
203
|
-
: undefined;
|
|
204
|
-
// Build query params
|
|
205
|
-
const queryParams = {
|
|
206
|
-
limit: PAGE_SIZE,
|
|
207
|
-
};
|
|
208
|
-
if (startingAfter) {
|
|
209
|
-
queryParams.starting_after = startingAfter;
|
|
210
|
-
}
|
|
211
|
-
if (status) {
|
|
212
|
-
queryParams.status = status;
|
|
213
|
-
}
|
|
214
|
-
if (searchQuery) {
|
|
215
|
-
queryParams.search = searchQuery;
|
|
216
|
-
}
|
|
217
|
-
// Fetch only the current page
|
|
218
|
-
const page = await client.devboxes.list(queryParams);
|
|
219
|
-
// Collect items from the page - only get PAGE_SIZE items, don't auto-paginate
|
|
220
|
-
let count = 0;
|
|
221
|
-
for await (const devbox of page) {
|
|
222
|
-
pageDevboxes.push(devbox);
|
|
223
|
-
count++;
|
|
224
|
-
// Break after getting PAGE_SIZE items to prevent auto-pagination
|
|
225
|
-
if (count >= PAGE_SIZE) {
|
|
226
|
-
break;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
// Update pagination metadata from the page object
|
|
230
|
-
// These properties are on the page object itself
|
|
231
|
-
const total = page.total_count || pageDevboxes.length;
|
|
232
|
-
const more = page.has_more || false;
|
|
233
|
-
setTotalCount(total);
|
|
234
|
-
setHasMore(more);
|
|
235
|
-
// Cache the page data and last ID
|
|
236
|
-
if (pageDevboxes.length > 0) {
|
|
237
|
-
pageCache.current.set(currentPage, pageDevboxes);
|
|
238
|
-
lastIdCache.current.set(currentPage, pageDevboxes[pageDevboxes.length - 1].id);
|
|
239
|
-
}
|
|
240
|
-
// Update devboxes for current page
|
|
241
|
-
setDevboxes((prev) => {
|
|
242
|
-
const hasChanged = JSON.stringify(prev) !== JSON.stringify(pageDevboxes);
|
|
243
|
-
return hasChanged ? pageDevboxes : prev;
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
catch (err) {
|
|
247
|
-
setError(err);
|
|
302
|
+
}, [devboxes.length, selectedIndex]);
|
|
303
|
+
const selectedDevbox = devboxes[selectedIndex];
|
|
304
|
+
// Calculate pagination info for display
|
|
305
|
+
const totalPages = Math.max(1, Math.ceil(totalCount / PAGE_SIZE));
|
|
306
|
+
const startIndex = currentPage * PAGE_SIZE;
|
|
307
|
+
const endIndex = startIndex + devboxes.length;
|
|
308
|
+
// Filter operations based on devbox status
|
|
309
|
+
const operations = selectedDevbox
|
|
310
|
+
? allOperations.filter((op) => {
|
|
311
|
+
const devboxStatus = selectedDevbox.status;
|
|
312
|
+
if (devboxStatus === "suspended") {
|
|
313
|
+
return op.key === "resume" || op.key === "logs";
|
|
248
314
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
// Only set initialLoading to false after first successful load
|
|
254
|
-
if (isInitialLoad) {
|
|
255
|
-
setInitialLoading(false);
|
|
256
|
-
setTimeout(() => setRefreshing(false), 300);
|
|
257
|
-
}
|
|
315
|
+
if (devboxStatus !== "running" &&
|
|
316
|
+
devboxStatus !== "provisioning" &&
|
|
317
|
+
devboxStatus !== "initializing") {
|
|
318
|
+
return op.key === "logs";
|
|
258
319
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const isFirstMount = initialLoading;
|
|
262
|
-
list(isFirstMount, false);
|
|
263
|
-
// Poll every 3 seconds (increased from 2), but only when in list view and not navigating
|
|
264
|
-
const interval = setInterval(() => {
|
|
265
|
-
if (!showDetails &&
|
|
266
|
-
!showCreate &&
|
|
267
|
-
!showActions &&
|
|
268
|
-
!isNavigating.current) {
|
|
269
|
-
// Don't clear cache on background refresh - just update the current page
|
|
270
|
-
list(false, true);
|
|
320
|
+
if (devboxStatus === "running") {
|
|
321
|
+
return op.key !== "resume";
|
|
271
322
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
// Animate refresh icon only when in list view
|
|
276
|
-
React.useEffect(() => {
|
|
277
|
-
if (showDetails || showCreate || showActions) {
|
|
278
|
-
return; // Don't animate when not in list view
|
|
279
|
-
}
|
|
280
|
-
const interval = setInterval(() => {
|
|
281
|
-
setRefreshIcon((prev) => (prev + 1) % 10);
|
|
282
|
-
}, 80);
|
|
283
|
-
return () => clearInterval(interval);
|
|
284
|
-
}, [showDetails, showCreate, showActions]);
|
|
323
|
+
return op.key === "logs" || op.key === "delete";
|
|
324
|
+
})
|
|
325
|
+
: allOperations;
|
|
285
326
|
useInput((input, key) => {
|
|
286
|
-
|
|
287
|
-
if (key.ctrl && input === "c") {
|
|
288
|
-
process.stdout.write("\x1b[?1049l"); // Exit alternate screen
|
|
289
|
-
process.exit(130);
|
|
290
|
-
}
|
|
291
|
-
const pageDevboxes = currentDevboxes.length;
|
|
327
|
+
const pageDevboxes = devboxes.length;
|
|
292
328
|
// Skip input handling when in search mode - let TextInput handle it
|
|
293
329
|
if (searchMode) {
|
|
294
330
|
if (key.escape) {
|
|
@@ -297,22 +333,21 @@ const ListDevboxesUI = ({ status, onSSHRequest, focusDevboxId, onBack, onExit })
|
|
|
297
333
|
}
|
|
298
334
|
return;
|
|
299
335
|
}
|
|
300
|
-
// Skip input handling when in details view
|
|
336
|
+
// Skip input handling when in details view
|
|
301
337
|
if (showDetails) {
|
|
302
338
|
return;
|
|
303
339
|
}
|
|
304
|
-
// Skip input handling when in create view
|
|
340
|
+
// Skip input handling when in create view
|
|
305
341
|
if (showCreate) {
|
|
306
342
|
return;
|
|
307
343
|
}
|
|
308
|
-
// Skip input handling when in actions view
|
|
344
|
+
// Skip input handling when in actions view
|
|
309
345
|
if (showActions) {
|
|
310
346
|
return;
|
|
311
347
|
}
|
|
312
348
|
// Handle popup navigation
|
|
313
349
|
if (showPopup) {
|
|
314
350
|
if (key.escape || input === "q") {
|
|
315
|
-
console.clear();
|
|
316
351
|
setShowPopup(false);
|
|
317
352
|
setSelectedOperation(0);
|
|
318
353
|
}
|
|
@@ -323,17 +358,13 @@ const ListDevboxesUI = ({ status, onSSHRequest, focusDevboxId, onBack, onExit })
|
|
|
323
358
|
setSelectedOperation(selectedOperation + 1);
|
|
324
359
|
}
|
|
325
360
|
else if (key.return) {
|
|
326
|
-
// Execute the selected operation
|
|
327
|
-
console.clear();
|
|
328
361
|
setShowPopup(false);
|
|
329
362
|
setShowActions(true);
|
|
330
363
|
}
|
|
331
364
|
else if (input) {
|
|
332
|
-
// Check for shortcut match
|
|
333
365
|
const matchedOpIndex = operations.findIndex((op) => op.shortcut === input);
|
|
334
366
|
if (matchedOpIndex !== -1) {
|
|
335
367
|
setSelectedOperation(matchedOpIndex);
|
|
336
|
-
console.clear();
|
|
337
368
|
setShowPopup(false);
|
|
338
369
|
setShowActions(true);
|
|
339
370
|
}
|
|
@@ -348,32 +379,35 @@ const ListDevboxesUI = ({ status, onSSHRequest, focusDevboxId, onBack, onExit })
|
|
|
348
379
|
setSelectedIndex(selectedIndex + 1);
|
|
349
380
|
}
|
|
350
381
|
else if ((input === "n" || key.rightArrow) &&
|
|
351
|
-
!
|
|
352
|
-
|
|
353
|
-
|
|
382
|
+
!loading &&
|
|
383
|
+
!navigating &&
|
|
384
|
+
hasMore) {
|
|
385
|
+
nextPage();
|
|
354
386
|
setSelectedIndex(0);
|
|
355
387
|
}
|
|
356
388
|
else if ((input === "p" || key.leftArrow) &&
|
|
357
|
-
!
|
|
358
|
-
|
|
359
|
-
|
|
389
|
+
!loading &&
|
|
390
|
+
!navigating &&
|
|
391
|
+
hasPrev) {
|
|
392
|
+
prevPage();
|
|
360
393
|
setSelectedIndex(0);
|
|
361
394
|
}
|
|
362
395
|
else if (key.return) {
|
|
363
|
-
|
|
364
|
-
|
|
396
|
+
if (onNavigateToDetail && selectedDevbox) {
|
|
397
|
+
onNavigateToDetail(selectedDevbox.id);
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
setShowDetails(true);
|
|
401
|
+
}
|
|
365
402
|
}
|
|
366
403
|
else if (input === "a") {
|
|
367
|
-
console.clear();
|
|
368
404
|
setShowPopup(true);
|
|
369
405
|
setSelectedOperation(0);
|
|
370
406
|
}
|
|
371
407
|
else if (input === "c") {
|
|
372
|
-
console.clear();
|
|
373
408
|
setShowCreate(true);
|
|
374
409
|
}
|
|
375
410
|
else if (input === "o" && selectedDevbox) {
|
|
376
|
-
// Open in browser
|
|
377
411
|
const url = getDevboxUrl(selectedDevbox.id);
|
|
378
412
|
const openBrowser = async () => {
|
|
379
413
|
const { exec } = await import("child_process");
|
|
@@ -396,14 +430,12 @@ const ListDevboxesUI = ({ status, onSSHRequest, focusDevboxId, onBack, onExit })
|
|
|
396
430
|
setSearchMode(true);
|
|
397
431
|
}
|
|
398
432
|
else if (key.escape) {
|
|
399
|
-
if (
|
|
400
|
-
|
|
433
|
+
if (submittedSearchQuery) {
|
|
434
|
+
setSubmittedSearchQuery("");
|
|
401
435
|
setSearchQuery("");
|
|
402
|
-
setCurrentPage(0);
|
|
403
436
|
setSelectedIndex(0);
|
|
404
437
|
}
|
|
405
438
|
else {
|
|
406
|
-
// Go back to home
|
|
407
439
|
if (onBack) {
|
|
408
440
|
onBack();
|
|
409
441
|
}
|
|
@@ -416,49 +448,12 @@ const ListDevboxesUI = ({ status, onSSHRequest, focusDevboxId, onBack, onExit })
|
|
|
416
448
|
}
|
|
417
449
|
}
|
|
418
450
|
});
|
|
419
|
-
// No client-side filtering - search is handled server-side
|
|
420
|
-
const currentDevboxes = devboxes;
|
|
421
|
-
// Ensure selected index is within bounds after filtering
|
|
422
|
-
React.useEffect(() => {
|
|
423
|
-
if (currentDevboxes.length > 0 && selectedIndex >= currentDevboxes.length) {
|
|
424
|
-
setSelectedIndex(Math.max(0, currentDevboxes.length - 1));
|
|
425
|
-
}
|
|
426
|
-
}, [currentDevboxes.length, selectedIndex]);
|
|
427
|
-
const selectedDevbox = currentDevboxes[selectedIndex];
|
|
428
|
-
// Calculate pagination info
|
|
429
|
-
const totalPages = Math.ceil(totalCount / PAGE_SIZE);
|
|
430
|
-
const startIndex = currentPage * PAGE_SIZE;
|
|
431
|
-
const endIndex = startIndex + currentDevboxes.length;
|
|
432
|
-
// Filter operations based on devbox status
|
|
433
|
-
const operations = selectedDevbox
|
|
434
|
-
? allOperations.filter((op) => {
|
|
435
|
-
const status = selectedDevbox.status;
|
|
436
|
-
// When suspended: logs and resume
|
|
437
|
-
if (status === "suspended") {
|
|
438
|
-
return op.key === "resume" || op.key === "logs";
|
|
439
|
-
}
|
|
440
|
-
// When not running (shutdown, failure, etc): only logs
|
|
441
|
-
if (status !== "running" &&
|
|
442
|
-
status !== "provisioning" &&
|
|
443
|
-
status !== "initializing") {
|
|
444
|
-
return op.key === "logs";
|
|
445
|
-
}
|
|
446
|
-
// When running: everything except resume
|
|
447
|
-
if (status === "running") {
|
|
448
|
-
return op.key !== "resume";
|
|
449
|
-
}
|
|
450
|
-
// Default for transitional states (provisioning, initializing)
|
|
451
|
-
return op.key === "logs" || op.key === "delete";
|
|
452
|
-
})
|
|
453
|
-
: allOperations;
|
|
454
451
|
// Create view
|
|
455
452
|
if (showCreate) {
|
|
456
453
|
return (_jsx(DevboxCreatePage, { onBack: () => {
|
|
457
454
|
setShowCreate(false);
|
|
458
|
-
}, onCreate: (
|
|
459
|
-
// Refresh the list after creation
|
|
455
|
+
}, onCreate: () => {
|
|
460
456
|
setShowCreate(false);
|
|
461
|
-
// The list will auto-refresh via the polling effect
|
|
462
457
|
} }));
|
|
463
458
|
}
|
|
464
459
|
// Actions view
|
|
@@ -470,203 +465,47 @@ const ListDevboxesUI = ({ status, onSSHRequest, focusDevboxId, onBack, onExit })
|
|
|
470
465
|
}, breadcrumbItems: [
|
|
471
466
|
{ label: "Devboxes" },
|
|
472
467
|
{ label: selectedDevbox.name || selectedDevbox.id, active: true },
|
|
473
|
-
], initialOperation: selectedOp?.key, skipOperationsMenu: true
|
|
468
|
+
], initialOperation: selectedOp?.key, skipOperationsMenu: true }));
|
|
474
469
|
}
|
|
475
470
|
// Details view
|
|
476
471
|
if (showDetails && selectedDevbox) {
|
|
477
|
-
return (_jsx(DevboxDetailPage, { devbox: selectedDevbox, onBack: () => setShowDetails(false)
|
|
472
|
+
return (_jsx(DevboxDetailPage, { devbox: selectedDevbox, onBack: () => setShowDetails(false) }));
|
|
478
473
|
}
|
|
479
|
-
//
|
|
480
|
-
if (
|
|
481
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes", active: true }]
|
|
482
|
-
{
|
|
483
|
-
key: "statusIcon",
|
|
484
|
-
label: "",
|
|
485
|
-
width: statusIconWidth,
|
|
486
|
-
render: (devbox, index, isSelected) => {
|
|
487
|
-
const statusDisplay = getStatusDisplay(devbox.status);
|
|
488
|
-
return (_jsxs(Text, { color: isSelected ? "white" : statusDisplay.color, bold: true, inverse: isSelected, wrap: "truncate", children: [statusDisplay.icon, " "] }));
|
|
489
|
-
},
|
|
490
|
-
},
|
|
491
|
-
createTextColumn("id", "ID", (devbox) => devbox.id, {
|
|
492
|
-
width: idWidth,
|
|
493
|
-
color: colors.textDim,
|
|
494
|
-
dimColor: false,
|
|
495
|
-
bold: false,
|
|
496
|
-
}),
|
|
497
|
-
{
|
|
498
|
-
key: "statusText",
|
|
499
|
-
label: "Status",
|
|
500
|
-
width: statusTextWidth,
|
|
501
|
-
render: (devbox, index, isSelected) => {
|
|
502
|
-
const statusDisplay = getStatusDisplay(devbox.status);
|
|
503
|
-
const truncated = statusDisplay.text.slice(0, statusTextWidth);
|
|
504
|
-
const padded = truncated.padEnd(statusTextWidth, " ");
|
|
505
|
-
return (_jsx(Text, { color: isSelected ? "white" : statusDisplay.color, bold: true, inverse: isSelected, wrap: "truncate", children: padded }));
|
|
506
|
-
},
|
|
507
|
-
},
|
|
508
|
-
createTextColumn("name", "Name", (devbox) => devbox.name || "", {
|
|
509
|
-
width: nameWidth,
|
|
510
|
-
dimColor: false,
|
|
511
|
-
}),
|
|
512
|
-
createTextColumn("capabilities", "Capabilities", (devbox) => {
|
|
513
|
-
const hasCapabilities = devbox.capabilities &&
|
|
514
|
-
devbox.capabilities.filter((c) => c !== "unknown")
|
|
515
|
-
.length > 0;
|
|
516
|
-
return hasCapabilities
|
|
517
|
-
? `[${devbox.capabilities
|
|
518
|
-
.filter((c) => c !== "unknown")
|
|
519
|
-
.map((c) => c === "computer_usage"
|
|
520
|
-
? "comp"
|
|
521
|
-
: c === "browser_usage"
|
|
522
|
-
? "browser"
|
|
523
|
-
: c === "docker_in_docker"
|
|
524
|
-
? "docker"
|
|
525
|
-
: c)
|
|
526
|
-
.join(",")}]`
|
|
527
|
-
: "";
|
|
528
|
-
}, {
|
|
529
|
-
width: capabilitiesWidth,
|
|
530
|
-
color: colors.info,
|
|
531
|
-
dimColor: false,
|
|
532
|
-
bold: false,
|
|
533
|
-
visible: showCapabilities,
|
|
534
|
-
}),
|
|
535
|
-
createTextColumn("source", "Source", (devbox) => devbox.blueprint_id
|
|
536
|
-
? devbox.blueprint_id
|
|
537
|
-
: devbox.snapshot_id
|
|
538
|
-
? devbox.snapshot_id
|
|
539
|
-
: "", {
|
|
540
|
-
width: sourceWidth,
|
|
541
|
-
color: colors.info,
|
|
542
|
-
dimColor: false,
|
|
543
|
-
bold: false,
|
|
544
|
-
visible: showSource,
|
|
545
|
-
}),
|
|
546
|
-
createTextColumn("created", "Created", (devbox) => devbox.create_time_ms
|
|
547
|
-
? formatTimeAgo(devbox.create_time_ms)
|
|
548
|
-
: "", {
|
|
549
|
-
width: timeWidth,
|
|
550
|
-
color: colors.textDim,
|
|
551
|
-
dimColor: false,
|
|
552
|
-
bold: false,
|
|
553
|
-
}),
|
|
554
|
-
] }) })), _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) }) })] }));
|
|
555
|
-
}
|
|
556
|
-
// If initial loading or error, show that first
|
|
557
|
-
if (initialLoading) {
|
|
558
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes", active: true }], showVersionCheck: true }), _jsx(SpinnerComponent, { message: "Loading..." })] }));
|
|
474
|
+
// Loading state (only on initial load)
|
|
475
|
+
if (loading && devboxes.length === 0) {
|
|
476
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes", active: true }] }), _jsx(SpinnerComponent, { message: "Loading..." })] }));
|
|
559
477
|
}
|
|
560
478
|
if (error) {
|
|
561
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes", active: true }]
|
|
479
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes", active: true }] }), _jsx(ErrorMessage, { message: "Failed to list devboxes", error: error })] }));
|
|
562
480
|
}
|
|
563
|
-
//
|
|
564
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes", active: true }] }),
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
label: "",
|
|
572
|
-
width: statusIconWidth,
|
|
573
|
-
render: (devbox, index, isSelected) => {
|
|
574
|
-
const statusDisplay = getStatusDisplay(devbox.status);
|
|
575
|
-
return (_jsxs(Text, { color: isSelected ? "white" : statusDisplay.color, bold: true, inverse: isSelected, wrap: "truncate", children: [statusDisplay.icon, " "] }));
|
|
576
|
-
},
|
|
577
|
-
},
|
|
578
|
-
createTextColumn("id", "ID", (devbox) => devbox.id, {
|
|
579
|
-
width: idWidth,
|
|
580
|
-
color: colors.textDim,
|
|
581
|
-
dimColor: false,
|
|
582
|
-
bold: false,
|
|
583
|
-
}),
|
|
584
|
-
{
|
|
585
|
-
key: "statusText",
|
|
586
|
-
label: "Status",
|
|
587
|
-
width: statusTextWidth,
|
|
588
|
-
render: (devbox, index, isSelected) => {
|
|
589
|
-
const statusDisplay = getStatusDisplay(devbox.status);
|
|
590
|
-
const truncated = statusDisplay.text.slice(0, statusTextWidth);
|
|
591
|
-
const padded = truncated.padEnd(statusTextWidth, " ");
|
|
592
|
-
return (_jsx(Text, { color: isSelected ? "white" : statusDisplay.color, bold: true, inverse: isSelected, wrap: "truncate", children: padded }));
|
|
593
|
-
},
|
|
594
|
-
},
|
|
595
|
-
createTextColumn("name", "Name", (devbox) => devbox.name || "", {
|
|
596
|
-
width: nameWidth,
|
|
597
|
-
}),
|
|
598
|
-
createTextColumn("capabilities", "Capabilities", (devbox) => {
|
|
599
|
-
const hasCapabilities = devbox.capabilities &&
|
|
600
|
-
devbox.capabilities.filter((c) => c !== "unknown")
|
|
601
|
-
.length > 0;
|
|
602
|
-
return hasCapabilities
|
|
603
|
-
? `[${devbox.capabilities
|
|
604
|
-
.filter((c) => c !== "unknown")
|
|
605
|
-
.map((c) => c === "computer_usage"
|
|
606
|
-
? "comp"
|
|
607
|
-
: c === "browser_usage"
|
|
608
|
-
? "browser"
|
|
609
|
-
: c === "docker_in_docker"
|
|
610
|
-
? "docker"
|
|
611
|
-
: c)
|
|
612
|
-
.join(",")}]`
|
|
613
|
-
: "";
|
|
614
|
-
}, {
|
|
615
|
-
width: capabilitiesWidth,
|
|
616
|
-
color: colors.info,
|
|
617
|
-
dimColor: false,
|
|
618
|
-
bold: false,
|
|
619
|
-
visible: showCapabilities,
|
|
620
|
-
}),
|
|
621
|
-
createTextColumn("source", "Source", (devbox) => devbox.blueprint_id
|
|
622
|
-
? devbox.blueprint_id
|
|
623
|
-
: devbox.snapshot_id
|
|
624
|
-
? devbox.snapshot_id
|
|
625
|
-
: "", {
|
|
626
|
-
width: sourceWidth,
|
|
627
|
-
color: colors.info,
|
|
628
|
-
dimColor: false,
|
|
629
|
-
bold: false,
|
|
630
|
-
visible: showSource,
|
|
631
|
-
}),
|
|
632
|
-
createTextColumn("created", "Created", (devbox) => devbox.create_time_ms
|
|
633
|
-
? formatTimeAgo(devbox.create_time_ms)
|
|
634
|
-
: "", {
|
|
635
|
-
width: timeWidth,
|
|
636
|
-
color: colors.textDim,
|
|
637
|
-
dimColor: false,
|
|
638
|
-
bold: false,
|
|
639
|
-
}),
|
|
640
|
-
] }, `table-${searchQuery}-${currentPage}`), _jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", totalCount] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "total"] }), totalPages > 1 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Page ", currentPage + 1, " of ", totalPages] })] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Showing ", startIndex + 1, "-", endIndex, " of ", totalCount] }), hasMore && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "(more available)"] })), _jsx(Text, { children: " " }), refreshing ? (_jsx(Text, { color: colors.primary, children: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"][refreshIcon % 10] })) : (_jsx(Text, { color: colors.success, children: figures.circleFilled }))] }), _jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate"] }), totalPages > 1 && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 ", figures.arrowLeft, figures.arrowRight, " Page"] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 [Enter] Details \u2022 [a] Actions \u2022 [c] Create \u2022 [/] Search \u2022 [o] Browser \u2022 [Esc] Back"] })] })] }))] }));
|
|
481
|
+
// Main list view
|
|
482
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes", active: true }] }), searchMode && (_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: colors.primary, children: [figures.pointerSmall, " Search: "] }), _jsx(TextInput, { value: searchQuery, onChange: setSearchQuery, placeholder: "Type to search...", onSubmit: () => {
|
|
483
|
+
setSearchMode(false);
|
|
484
|
+
setSubmittedSearchQuery(searchQuery);
|
|
485
|
+
setSelectedIndex(0);
|
|
486
|
+
} }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[Enter to search, Esc to cancel]"] })] })), !searchMode && submittedSearchQuery && (_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: colors.primary, children: [figures.info, " Searching for: "] }), _jsx(Text, { color: colors.warning, bold: true, children: submittedSearchQuery.length > 50
|
|
487
|
+
? submittedSearchQuery.substring(0, 50) + "..."
|
|
488
|
+
: submittedSearchQuery }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "(", totalCount, " results) [/ to edit, Esc to clear]"] })] })), !showPopup && (_jsx(Table, { data: devboxes, keyExtractor: (devbox) => devbox.id, selectedIndex: selectedIndex, title: "devboxes", columns: tableColumns })), !showPopup && (_jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", totalCount] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "total"] }), totalPages > 1 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), navigating ? (_jsxs(Text, { color: colors.warning, children: [figures.pointer, " Loading page ", currentPage + 1, "..."] })) : (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Page ", currentPage + 1, " of ", totalPages] }))] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Showing ", startIndex + 1, "-", endIndex, " of ", totalCount] }), submittedSearchQuery && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.warning, children: ["Filtered: \"", submittedSearchQuery, "\""] })] }))] })), showPopup && selectedDevbox && (_jsx(Box, { marginTop: 2, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedDevbox, operations: operations, selectedOperation: selectedOperation, onClose: () => setShowPopup(false) }) })), _jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate"] }), (hasMore || hasPrev) && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 ", figures.arrowLeft, figures.arrowRight, " Page"] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 [Enter] Details"] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 [a] Actions"] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 [c] Create"] }), selectedDevbox && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 [o] Open in Browser"] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 [/] Search"] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 [Esc] Back"] })] })] }));
|
|
641
489
|
};
|
|
642
490
|
// Export the UI component for use in the main menu
|
|
643
491
|
export { ListDevboxesUI };
|
|
644
|
-
export async function listDevboxes(options
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
const
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
limit: DEFAULT_PAGE_SIZE,
|
|
654
|
-
});
|
|
655
|
-
}, () => (_jsx(ListDevboxesUI, { status: options.status, focusDevboxId: focusDevboxId, onSSHRequest: (config) => {
|
|
656
|
-
sshSessionConfig = config;
|
|
657
|
-
} })), DEFAULT_PAGE_SIZE);
|
|
658
|
-
// If SSH was requested, handle it now after Ink has exited
|
|
659
|
-
if (sshSessionConfig) {
|
|
660
|
-
const result = await runSSHSession(sshSessionConfig);
|
|
661
|
-
if (result.shouldRestart) {
|
|
662
|
-
console.clear();
|
|
663
|
-
console.log(`\nSSH session ended. Returning to CLI...\n`);
|
|
664
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
665
|
-
// Restart the list view with the devbox ID to focus on
|
|
666
|
-
await listDevboxes(options, result.returnToDevboxId);
|
|
667
|
-
}
|
|
668
|
-
else {
|
|
669
|
-
process.exit(result.exitCode);
|
|
492
|
+
export async function listDevboxes(options) {
|
|
493
|
+
try {
|
|
494
|
+
const client = getClient();
|
|
495
|
+
// Build query params
|
|
496
|
+
const queryParams = {
|
|
497
|
+
limit: options.limit ? parseInt(options.limit, 10) : DEFAULT_PAGE_SIZE,
|
|
498
|
+
};
|
|
499
|
+
if (options.status) {
|
|
500
|
+
queryParams.status = options.status;
|
|
670
501
|
}
|
|
502
|
+
// Fetch devboxes
|
|
503
|
+
const page = (await client.devboxes.list(queryParams));
|
|
504
|
+
// Extract devboxes array
|
|
505
|
+
const devboxes = page.devboxes || [];
|
|
506
|
+
output(devboxes, { format: options.output, defaultFormat: "json" });
|
|
507
|
+
}
|
|
508
|
+
catch (error) {
|
|
509
|
+
outputError("Failed to list devboxes", error);
|
|
671
510
|
}
|
|
672
511
|
}
|