@runloop/rl-cli 0.0.3 → 0.1.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/README.md +64 -29
- package/dist/cli.js +401 -92
- package/dist/commands/auth.js +12 -11
- package/dist/commands/blueprint/create.js +108 -0
- package/dist/commands/blueprint/get.js +37 -0
- package/dist/commands/blueprint/list.js +293 -225
- package/dist/commands/blueprint/logs.js +40 -0
- package/dist/commands/blueprint/preview.js +45 -0
- package/dist/commands/devbox/create.js +10 -9
- package/dist/commands/devbox/delete.js +8 -8
- package/dist/commands/devbox/download.js +49 -0
- package/dist/commands/devbox/exec.js +23 -13
- package/dist/commands/devbox/execAsync.js +43 -0
- package/dist/commands/devbox/get.js +37 -0
- package/dist/commands/devbox/getAsync.js +37 -0
- package/dist/commands/devbox/list.js +328 -190
- package/dist/commands/devbox/logs.js +40 -0
- package/dist/commands/devbox/read.js +49 -0
- package/dist/commands/devbox/resume.js +37 -0
- package/dist/commands/devbox/rsync.js +118 -0
- package/dist/commands/devbox/scp.js +122 -0
- package/dist/commands/devbox/shutdown.js +37 -0
- package/dist/commands/devbox/ssh.js +104 -0
- package/dist/commands/devbox/suspend.js +37 -0
- package/dist/commands/devbox/tunnel.js +120 -0
- package/dist/commands/devbox/upload.js +10 -10
- package/dist/commands/devbox/write.js +51 -0
- package/dist/commands/mcp-http.js +37 -0
- package/dist/commands/mcp-install.js +120 -0
- package/dist/commands/mcp.js +30 -0
- package/dist/commands/menu.js +20 -20
- package/dist/commands/object/delete.js +37 -0
- package/dist/commands/object/download.js +88 -0
- package/dist/commands/object/get.js +37 -0
- package/dist/commands/object/list.js +112 -0
- package/dist/commands/object/upload.js +130 -0
- package/dist/commands/snapshot/create.js +12 -11
- package/dist/commands/snapshot/delete.js +8 -8
- package/dist/commands/snapshot/list.js +56 -97
- package/dist/commands/snapshot/status.js +37 -0
- package/dist/components/ActionsPopup.js +16 -13
- package/dist/components/Banner.js +4 -4
- package/dist/components/Breadcrumb.js +55 -5
- package/dist/components/DetailView.js +7 -4
- package/dist/components/DevboxActionsMenu.js +315 -178
- package/dist/components/DevboxCard.js +15 -14
- package/dist/components/DevboxCreatePage.js +147 -113
- package/dist/components/DevboxDetailPage.js +180 -102
- package/dist/components/ErrorMessage.js +5 -4
- package/dist/components/Header.js +4 -3
- package/dist/components/MainMenu.js +34 -33
- package/dist/components/MetadataDisplay.js +17 -9
- package/dist/components/OperationsMenu.js +6 -5
- package/dist/components/ResourceActionsMenu.js +117 -0
- package/dist/components/ResourceListView.js +213 -0
- package/dist/components/Spinner.js +5 -4
- package/dist/components/StatusBadge.js +81 -31
- package/dist/components/SuccessMessage.js +4 -3
- package/dist/components/Table.example.js +53 -23
- package/dist/components/Table.js +19 -11
- package/dist/hooks/useCursorPagination.js +125 -0
- package/dist/mcp/server-http.js +416 -0
- package/dist/mcp/server.js +397 -0
- package/dist/utils/CommandExecutor.js +16 -12
- package/dist/utils/client.js +7 -7
- package/dist/utils/config.js +130 -4
- package/dist/utils/interactiveCommand.js +2 -2
- package/dist/utils/output.js +17 -17
- package/dist/utils/ssh.js +160 -0
- package/dist/utils/sshSession.js +16 -12
- package/dist/utils/theme.js +22 -0
- package/dist/utils/url.js +4 -4
- package/package.json +29 -4
|
@@ -1,46 +1,28 @@
|
|
|
1
|
-
import { jsx as _jsx,
|
|
2
|
-
import React from
|
|
3
|
-
import { Box, Text, useInput, useApp, useStdout } from
|
|
4
|
-
import TextInput from
|
|
5
|
-
import figures from
|
|
6
|
-
import { getClient } from
|
|
7
|
-
import { SpinnerComponent } from
|
|
8
|
-
import { ErrorMessage } from
|
|
9
|
-
import { getStatusDisplay } from
|
|
10
|
-
import { Breadcrumb } from
|
|
11
|
-
import { Table, createTextColumn } from
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const seconds = Math.floor((Date.now() - timestamp) / 1000);
|
|
22
|
-
if (seconds < 60)
|
|
23
|
-
return `${seconds}s ago`;
|
|
24
|
-
const minutes = Math.floor(seconds / 60);
|
|
25
|
-
if (minutes < 60)
|
|
26
|
-
return `${minutes}m ago`;
|
|
27
|
-
const hours = Math.floor(minutes / 60);
|
|
28
|
-
if (hours < 24)
|
|
29
|
-
return `${hours}h ago`;
|
|
30
|
-
const days = Math.floor(hours / 24);
|
|
31
|
-
if (days < 30)
|
|
32
|
-
return `${days}d ago`;
|
|
33
|
-
const months = Math.floor(days / 30);
|
|
34
|
-
if (months < 12)
|
|
35
|
-
return `${months}mo ago`;
|
|
36
|
-
const years = Math.floor(months / 12);
|
|
37
|
-
return `${years}y ago`;
|
|
38
|
-
};
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { Box, Text, useInput, useApp, useStdout } from "ink";
|
|
4
|
+
import TextInput from "ink-text-input";
|
|
5
|
+
import figures from "figures";
|
|
6
|
+
import { getClient } from "../../utils/client.js";
|
|
7
|
+
import { SpinnerComponent } from "../../components/Spinner.js";
|
|
8
|
+
import { ErrorMessage } from "../../components/ErrorMessage.js";
|
|
9
|
+
import { getStatusDisplay } from "../../components/StatusBadge.js";
|
|
10
|
+
import { Breadcrumb } from "../../components/Breadcrumb.js";
|
|
11
|
+
import { Table, createTextColumn } from "../../components/Table.js";
|
|
12
|
+
import { formatTimeAgo } from "../../components/ResourceListView.js";
|
|
13
|
+
import { createExecutor } from "../../utils/CommandExecutor.js";
|
|
14
|
+
import { DevboxDetailPage } from "../../components/DevboxDetailPage.js";
|
|
15
|
+
import { DevboxCreatePage } from "../../components/DevboxCreatePage.js";
|
|
16
|
+
import { ResourceActionsMenu } from "../../components/ResourceActionsMenu.js";
|
|
17
|
+
import { ActionsPopup } from "../../components/ActionsPopup.js";
|
|
18
|
+
import { getDevboxUrl } from "../../utils/url.js";
|
|
19
|
+
import { runSSHSession, } from "../../utils/sshSession.js";
|
|
20
|
+
import { colors } from "../../utils/theme.js";
|
|
39
21
|
const DEFAULT_PAGE_SIZE = 10;
|
|
40
22
|
const ListDevboxesUI = ({ status, onSSHRequest, focusDevboxId, onBack, onExit }) => {
|
|
41
23
|
const { exit: inkExit } = useApp();
|
|
42
24
|
const { stdout } = useStdout();
|
|
43
|
-
const [
|
|
25
|
+
const [initialLoading, setInitialLoading] = React.useState(true);
|
|
44
26
|
const [devboxes, setDevboxes] = React.useState([]);
|
|
45
27
|
const [error, setError] = React.useState(null);
|
|
46
28
|
const [currentPage, setCurrentPage] = React.useState(0);
|
|
@@ -52,8 +34,9 @@ const ListDevboxesUI = ({ status, onSSHRequest, focusDevboxId, onBack, onExit })
|
|
|
52
34
|
const [selectedOperation, setSelectedOperation] = React.useState(0);
|
|
53
35
|
const [refreshing, setRefreshing] = React.useState(false);
|
|
54
36
|
const [refreshIcon, setRefreshIcon] = React.useState(0);
|
|
37
|
+
const isNavigating = React.useRef(false);
|
|
55
38
|
const [searchMode, setSearchMode] = React.useState(false);
|
|
56
|
-
const [searchQuery, setSearchQuery] = React.useState(
|
|
39
|
+
const [searchQuery, setSearchQuery] = React.useState("");
|
|
57
40
|
const [totalCount, setTotalCount] = React.useState(0);
|
|
58
41
|
const [hasMore, setHasMore] = React.useState(false);
|
|
59
42
|
const pageCache = React.useRef(new Map());
|
|
@@ -69,66 +52,155 @@ const ListDevboxesUI = ({ status, onSSHRequest, focusDevboxId, onBack, onExit })
|
|
|
69
52
|
const statusTextWidth = 10;
|
|
70
53
|
const timeWidth = 20;
|
|
71
54
|
const capabilitiesWidth = 18;
|
|
72
|
-
const
|
|
55
|
+
const sourceWidth = 26;
|
|
73
56
|
// ID is always full width (25 chars for dbx_31CYd5LLFbBxst8mqnUjO format)
|
|
74
57
|
const idWidth = 26;
|
|
75
58
|
// Responsive layout based on terminal width
|
|
76
|
-
const showCapabilities = terminalWidth >=
|
|
77
|
-
const
|
|
59
|
+
const showCapabilities = terminalWidth >= 140;
|
|
60
|
+
const showSource = terminalWidth >= 120;
|
|
78
61
|
// Name width is flexible and uses remaining space
|
|
79
62
|
let nameWidth = 15;
|
|
80
63
|
if (terminalWidth >= 120) {
|
|
81
|
-
const remainingWidth = terminalWidth -
|
|
64
|
+
const remainingWidth = terminalWidth -
|
|
65
|
+
fixedWidth -
|
|
66
|
+
statusIconWidth -
|
|
67
|
+
idWidth -
|
|
68
|
+
statusTextWidth -
|
|
69
|
+
timeWidth -
|
|
70
|
+
capabilitiesWidth -
|
|
71
|
+
sourceWidth -
|
|
72
|
+
12;
|
|
82
73
|
nameWidth = Math.max(15, remainingWidth);
|
|
83
74
|
}
|
|
84
75
|
else if (terminalWidth >= 110) {
|
|
85
|
-
const remainingWidth = terminalWidth -
|
|
76
|
+
const remainingWidth = terminalWidth -
|
|
77
|
+
fixedWidth -
|
|
78
|
+
statusIconWidth -
|
|
79
|
+
idWidth -
|
|
80
|
+
statusTextWidth -
|
|
81
|
+
timeWidth -
|
|
82
|
+
sourceWidth -
|
|
83
|
+
10;
|
|
86
84
|
nameWidth = Math.max(12, remainingWidth);
|
|
87
85
|
}
|
|
88
86
|
else {
|
|
89
|
-
const remainingWidth = terminalWidth -
|
|
87
|
+
const remainingWidth = terminalWidth -
|
|
88
|
+
fixedWidth -
|
|
89
|
+
statusIconWidth -
|
|
90
|
+
idWidth -
|
|
91
|
+
statusTextWidth -
|
|
92
|
+
timeWidth -
|
|
93
|
+
10;
|
|
90
94
|
nameWidth = Math.max(8, remainingWidth);
|
|
91
95
|
}
|
|
92
96
|
// Define allOperations
|
|
93
97
|
const allOperations = [
|
|
94
|
-
{
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
{
|
|
102
|
-
|
|
98
|
+
{
|
|
99
|
+
key: "logs",
|
|
100
|
+
label: "View Logs",
|
|
101
|
+
color: colors.info,
|
|
102
|
+
icon: figures.info,
|
|
103
|
+
shortcut: "l",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
key: "exec",
|
|
107
|
+
label: "Execute Command",
|
|
108
|
+
color: colors.success,
|
|
109
|
+
icon: figures.play,
|
|
110
|
+
shortcut: "e",
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
key: "upload",
|
|
114
|
+
label: "Upload File",
|
|
115
|
+
color: colors.success,
|
|
116
|
+
icon: figures.arrowUp,
|
|
117
|
+
shortcut: "u",
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
key: "snapshot",
|
|
121
|
+
label: "Create Snapshot",
|
|
122
|
+
color: colors.warning,
|
|
123
|
+
icon: figures.circleFilled,
|
|
124
|
+
shortcut: "n",
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
key: "ssh",
|
|
128
|
+
label: "SSH onto the box",
|
|
129
|
+
color: colors.primary,
|
|
130
|
+
icon: figures.arrowRight,
|
|
131
|
+
shortcut: "s",
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
key: "tunnel",
|
|
135
|
+
label: "Open Tunnel",
|
|
136
|
+
color: colors.secondary,
|
|
137
|
+
icon: figures.pointerSmall,
|
|
138
|
+
shortcut: "t",
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
key: "suspend",
|
|
142
|
+
label: "Suspend Devbox",
|
|
143
|
+
color: colors.warning,
|
|
144
|
+
icon: figures.squareSmallFilled,
|
|
145
|
+
shortcut: "p",
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
key: "resume",
|
|
149
|
+
label: "Resume Devbox",
|
|
150
|
+
color: colors.success,
|
|
151
|
+
icon: figures.play,
|
|
152
|
+
shortcut: "r",
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
key: "delete",
|
|
156
|
+
label: "Shutdown Devbox",
|
|
157
|
+
color: colors.error,
|
|
158
|
+
icon: figures.cross,
|
|
159
|
+
shortcut: "d",
|
|
160
|
+
},
|
|
103
161
|
];
|
|
104
162
|
// Check if we need to focus on a specific devbox after returning from SSH
|
|
105
163
|
React.useEffect(() => {
|
|
106
|
-
if (focusDevboxId && devboxes.length > 0 && !
|
|
164
|
+
if (focusDevboxId && devboxes.length > 0 && !initialLoading) {
|
|
107
165
|
// Find the devbox in the current page
|
|
108
|
-
const devboxIndex = devboxes.findIndex(d => d.id === focusDevboxId);
|
|
166
|
+
const devboxIndex = devboxes.findIndex((d) => d.id === focusDevboxId);
|
|
109
167
|
if (devboxIndex !== -1) {
|
|
110
168
|
setSelectedIndex(devboxIndex);
|
|
111
169
|
setShowDetails(true);
|
|
112
170
|
}
|
|
113
171
|
}
|
|
114
|
-
}, [devboxes,
|
|
172
|
+
}, [devboxes, initialLoading, focusDevboxId]);
|
|
173
|
+
// Clear cache when search query changes
|
|
174
|
+
React.useEffect(() => {
|
|
175
|
+
pageCache.current.clear();
|
|
176
|
+
lastIdCache.current.clear();
|
|
177
|
+
setCurrentPage(0);
|
|
178
|
+
}, [searchQuery]);
|
|
115
179
|
React.useEffect(() => {
|
|
116
|
-
const list = async (isInitialLoad = false) => {
|
|
180
|
+
const list = async (isInitialLoad = false, isBackgroundRefresh = false) => {
|
|
117
181
|
try {
|
|
182
|
+
// Set navigating flag at the start (but not for background refresh)
|
|
183
|
+
if (!isBackgroundRefresh) {
|
|
184
|
+
isNavigating.current = true;
|
|
185
|
+
}
|
|
118
186
|
// Only show refreshing indicator on initial load
|
|
119
187
|
if (isInitialLoad) {
|
|
120
188
|
setRefreshing(true);
|
|
121
189
|
}
|
|
122
190
|
// Check if we have cached data for this page
|
|
123
|
-
if (!isInitialLoad &&
|
|
191
|
+
if (!isInitialLoad &&
|
|
192
|
+
!isBackgroundRefresh &&
|
|
193
|
+
pageCache.current.has(currentPage)) {
|
|
124
194
|
setDevboxes(pageCache.current.get(currentPage) || []);
|
|
125
|
-
|
|
195
|
+
isNavigating.current = false;
|
|
126
196
|
return;
|
|
127
197
|
}
|
|
128
198
|
const client = getClient();
|
|
129
199
|
const pageDevboxes = [];
|
|
130
200
|
// Get starting_after cursor from previous page's last ID
|
|
131
|
-
const startingAfter = currentPage > 0
|
|
201
|
+
const startingAfter = currentPage > 0
|
|
202
|
+
? lastIdCache.current.get(currentPage - 1)
|
|
203
|
+
: undefined;
|
|
132
204
|
// Build query params
|
|
133
205
|
const queryParams = {
|
|
134
206
|
limit: PAGE_SIZE,
|
|
@@ -139,6 +211,9 @@ const ListDevboxesUI = ({ status, onSSHRequest, focusDevboxId, onBack, onExit })
|
|
|
139
211
|
if (status) {
|
|
140
212
|
queryParams.status = status;
|
|
141
213
|
}
|
|
214
|
+
if (searchQuery) {
|
|
215
|
+
queryParams.search = searchQuery;
|
|
216
|
+
}
|
|
142
217
|
// Fetch only the current page
|
|
143
218
|
const page = await client.devboxes.list(queryParams);
|
|
144
219
|
// Collect items from the page - only get PAGE_SIZE items, don't auto-paginate
|
|
@@ -172,25 +247,31 @@ const ListDevboxesUI = ({ status, onSSHRequest, focusDevboxId, onBack, onExit })
|
|
|
172
247
|
setError(err);
|
|
173
248
|
}
|
|
174
249
|
finally {
|
|
175
|
-
|
|
176
|
-
|
|
250
|
+
if (!isBackgroundRefresh) {
|
|
251
|
+
isNavigating.current = false;
|
|
252
|
+
}
|
|
253
|
+
// Only set initialLoading to false after first successful load
|
|
177
254
|
if (isInitialLoad) {
|
|
255
|
+
setInitialLoading(false);
|
|
178
256
|
setTimeout(() => setRefreshing(false), 300);
|
|
179
257
|
}
|
|
180
258
|
}
|
|
181
259
|
};
|
|
182
|
-
|
|
183
|
-
|
|
260
|
+
// Only treat as initial load on first mount
|
|
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
|
|
184
264
|
const interval = setInterval(() => {
|
|
185
|
-
if (!showDetails &&
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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);
|
|
190
271
|
}
|
|
191
272
|
}, 3000);
|
|
192
273
|
return () => clearInterval(interval);
|
|
193
|
-
}, [showDetails, showCreate, showActions, currentPage]);
|
|
274
|
+
}, [showDetails, showCreate, showActions, currentPage, searchQuery]);
|
|
194
275
|
// Animate refresh icon only when in list view
|
|
195
276
|
React.useEffect(() => {
|
|
196
277
|
if (showDetails || showCreate || showActions) {
|
|
@@ -203,8 +284,8 @@ const ListDevboxesUI = ({ status, onSSHRequest, focusDevboxId, onBack, onExit })
|
|
|
203
284
|
}, [showDetails, showCreate, showActions]);
|
|
204
285
|
useInput((input, key) => {
|
|
205
286
|
// Handle Ctrl+C to force exit
|
|
206
|
-
if (key.ctrl && input ===
|
|
207
|
-
process.stdout.write(
|
|
287
|
+
if (key.ctrl && input === "c") {
|
|
288
|
+
process.stdout.write("\x1b[?1049l"); // Exit alternate screen
|
|
208
289
|
process.exit(130);
|
|
209
290
|
}
|
|
210
291
|
const pageDevboxes = currentDevboxes.length;
|
|
@@ -212,7 +293,7 @@ const ListDevboxesUI = ({ status, onSSHRequest, focusDevboxId, onBack, onExit })
|
|
|
212
293
|
if (searchMode) {
|
|
213
294
|
if (key.escape) {
|
|
214
295
|
setSearchMode(false);
|
|
215
|
-
setSearchQuery(
|
|
296
|
+
setSearchQuery("");
|
|
216
297
|
}
|
|
217
298
|
return;
|
|
218
299
|
}
|
|
@@ -230,7 +311,7 @@ const ListDevboxesUI = ({ status, onSSHRequest, focusDevboxId, onBack, onExit })
|
|
|
230
311
|
}
|
|
231
312
|
// Handle popup navigation
|
|
232
313
|
if (showPopup) {
|
|
233
|
-
if (key.escape || input ===
|
|
314
|
+
if (key.escape || input === "q") {
|
|
234
315
|
console.clear();
|
|
235
316
|
setShowPopup(false);
|
|
236
317
|
setSelectedOperation(0);
|
|
@@ -249,7 +330,7 @@ const ListDevboxesUI = ({ status, onSSHRequest, focusDevboxId, onBack, onExit })
|
|
|
249
330
|
}
|
|
250
331
|
else if (input) {
|
|
251
332
|
// Check for shortcut match
|
|
252
|
-
const matchedOpIndex = operations.findIndex(op => op.shortcut === input);
|
|
333
|
+
const matchedOpIndex = operations.findIndex((op) => op.shortcut === input);
|
|
253
334
|
if (matchedOpIndex !== -1) {
|
|
254
335
|
setSelectedOperation(matchedOpIndex);
|
|
255
336
|
console.clear();
|
|
@@ -266,11 +347,15 @@ const ListDevboxesUI = ({ status, onSSHRequest, focusDevboxId, onBack, onExit })
|
|
|
266
347
|
else if (key.downArrow && selectedIndex < pageDevboxes - 1) {
|
|
267
348
|
setSelectedIndex(selectedIndex + 1);
|
|
268
349
|
}
|
|
269
|
-
else if ((input ===
|
|
350
|
+
else if ((input === "n" || key.rightArrow) &&
|
|
351
|
+
!isNavigating.current &&
|
|
352
|
+
currentPage < totalPages - 1) {
|
|
270
353
|
setCurrentPage(currentPage + 1);
|
|
271
354
|
setSelectedIndex(0);
|
|
272
355
|
}
|
|
273
|
-
else if ((input ===
|
|
356
|
+
else if ((input === "p" || key.leftArrow) &&
|
|
357
|
+
!isNavigating.current &&
|
|
358
|
+
currentPage > 0) {
|
|
274
359
|
setCurrentPage(currentPage - 1);
|
|
275
360
|
setSelectedIndex(0);
|
|
276
361
|
}
|
|
@@ -278,26 +363,26 @@ const ListDevboxesUI = ({ status, onSSHRequest, focusDevboxId, onBack, onExit })
|
|
|
278
363
|
console.clear();
|
|
279
364
|
setShowDetails(true);
|
|
280
365
|
}
|
|
281
|
-
else if (input ===
|
|
366
|
+
else if (input === "a") {
|
|
282
367
|
console.clear();
|
|
283
368
|
setShowPopup(true);
|
|
284
369
|
setSelectedOperation(0);
|
|
285
370
|
}
|
|
286
|
-
else if (input ===
|
|
371
|
+
else if (input === "c") {
|
|
287
372
|
console.clear();
|
|
288
373
|
setShowCreate(true);
|
|
289
374
|
}
|
|
290
|
-
else if (input ===
|
|
375
|
+
else if (input === "o" && selectedDevbox) {
|
|
291
376
|
// Open in browser
|
|
292
377
|
const url = getDevboxUrl(selectedDevbox.id);
|
|
293
378
|
const openBrowser = async () => {
|
|
294
|
-
const { exec } = await import(
|
|
379
|
+
const { exec } = await import("child_process");
|
|
295
380
|
const platform = process.platform;
|
|
296
381
|
let openCommand;
|
|
297
|
-
if (platform ===
|
|
382
|
+
if (platform === "darwin") {
|
|
298
383
|
openCommand = `open "${url}"`;
|
|
299
384
|
}
|
|
300
|
-
else if (platform ===
|
|
385
|
+
else if (platform === "win32") {
|
|
301
386
|
openCommand = `start "${url}"`;
|
|
302
387
|
}
|
|
303
388
|
else {
|
|
@@ -307,13 +392,13 @@ const ListDevboxesUI = ({ status, onSSHRequest, focusDevboxId, onBack, onExit })
|
|
|
307
392
|
};
|
|
308
393
|
openBrowser();
|
|
309
394
|
}
|
|
310
|
-
else if (input ===
|
|
395
|
+
else if (input === "/") {
|
|
311
396
|
setSearchMode(true);
|
|
312
397
|
}
|
|
313
398
|
else if (key.escape) {
|
|
314
399
|
if (searchQuery) {
|
|
315
400
|
// Clear search when Esc is pressed and there's an active search
|
|
316
|
-
setSearchQuery(
|
|
401
|
+
setSearchQuery("");
|
|
317
402
|
setCurrentPage(0);
|
|
318
403
|
setSelectedIndex(0);
|
|
319
404
|
}
|
|
@@ -331,19 +416,8 @@ const ListDevboxesUI = ({ status, onSSHRequest, focusDevboxId, onBack, onExit })
|
|
|
331
416
|
}
|
|
332
417
|
}
|
|
333
418
|
});
|
|
334
|
-
//
|
|
335
|
-
const
|
|
336
|
-
if (!searchQuery.trim())
|
|
337
|
-
return devboxes;
|
|
338
|
-
const query = searchQuery.toLowerCase();
|
|
339
|
-
return devboxes.filter(devbox => {
|
|
340
|
-
return (devbox.id?.toLowerCase().includes(query) ||
|
|
341
|
-
devbox.name?.toLowerCase().includes(query) ||
|
|
342
|
-
devbox.status?.toLowerCase().includes(query));
|
|
343
|
-
});
|
|
344
|
-
}, [devboxes, searchQuery]);
|
|
345
|
-
// Current page is already fetched, no need to slice
|
|
346
|
-
const currentDevboxes = filteredDevboxes;
|
|
419
|
+
// No client-side filtering - search is handled server-side
|
|
420
|
+
const currentDevboxes = devboxes;
|
|
347
421
|
// Ensure selected index is within bounds after filtering
|
|
348
422
|
React.useEffect(() => {
|
|
349
423
|
if (currentDevboxes.length > 0 && selectedIndex >= currentDevboxes.length) {
|
|
@@ -356,23 +430,27 @@ const ListDevboxesUI = ({ status, onSSHRequest, focusDevboxId, onBack, onExit })
|
|
|
356
430
|
const startIndex = currentPage * PAGE_SIZE;
|
|
357
431
|
const endIndex = startIndex + currentDevboxes.length;
|
|
358
432
|
// Filter operations based on devbox status
|
|
359
|
-
const operations = selectedDevbox
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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;
|
|
376
454
|
// Create view
|
|
377
455
|
if (showCreate) {
|
|
378
456
|
return (_jsx(DevboxCreatePage, { onBack: () => {
|
|
@@ -386,12 +464,12 @@ const ListDevboxesUI = ({ status, onSSHRequest, focusDevboxId, onBack, onExit })
|
|
|
386
464
|
// Actions view
|
|
387
465
|
if (showActions && selectedDevbox) {
|
|
388
466
|
const selectedOp = operations[selectedOperation];
|
|
389
|
-
return (_jsx(
|
|
467
|
+
return (_jsx(ResourceActionsMenu, { resourceType: "devbox", resource: selectedDevbox, onBack: () => {
|
|
390
468
|
setShowActions(false);
|
|
391
469
|
setSelectedOperation(0);
|
|
392
470
|
}, breadcrumbItems: [
|
|
393
|
-
{ label:
|
|
394
|
-
{ label: selectedDevbox.name || selectedDevbox.id, active: true }
|
|
471
|
+
{ label: "Devboxes" },
|
|
472
|
+
{ label: selectedDevbox.name || selectedDevbox.id, active: true },
|
|
395
473
|
], initialOperation: selectedOp?.key, skipOperationsMenu: true, onSSHRequest: onSSHRequest }));
|
|
396
474
|
}
|
|
397
475
|
// Details view
|
|
@@ -400,108 +478,166 @@ const ListDevboxesUI = ({ status, onSSHRequest, focusDevboxId, onBack, onExit })
|
|
|
400
478
|
}
|
|
401
479
|
// Show popup with table in background
|
|
402
480
|
if (showPopup && selectedDevbox) {
|
|
403
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
404
|
-
{ label: 'Devboxes', active: true }
|
|
405
|
-
] }), !loading && !error && devboxes.length > 0 && (_jsx(_Fragment, { children: _jsx(Table, { data: currentDevboxes, keyExtractor: (devbox) => devbox.id, selectedIndex: selectedIndex, title: `devboxes[${totalCount}]`, columns: [
|
|
481
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes", active: true }], showVersionCheck: true }), !initialLoading && !error && devboxes.length > 0 && (_jsx(_Fragment, { children: _jsx(Table, { data: currentDevboxes, keyExtractor: (devbox) => devbox.id, selectedIndex: selectedIndex, title: `devboxes[${totalCount}]`, columns: [
|
|
406
482
|
{
|
|
407
|
-
key:
|
|
408
|
-
label:
|
|
483
|
+
key: "statusIcon",
|
|
484
|
+
label: "",
|
|
409
485
|
width: statusIconWidth,
|
|
410
486
|
render: (devbox, index, isSelected) => {
|
|
411
487
|
const statusDisplay = getStatusDisplay(devbox.status);
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
const padded = icon.padEnd(statusIconWidth, ' ');
|
|
415
|
-
return (_jsx(Text, { color: isSelected ? 'white' : statusDisplay.color, bold: true, inverse: isSelected, children: padded }));
|
|
416
|
-
}
|
|
488
|
+
return (_jsxs(Text, { color: isSelected ? "white" : statusDisplay.color, bold: true, inverse: isSelected, wrap: "truncate", children: [statusDisplay.icon, " "] }));
|
|
489
|
+
},
|
|
417
490
|
},
|
|
418
|
-
createTextColumn(
|
|
491
|
+
createTextColumn("id", "ID", (devbox) => devbox.id, {
|
|
492
|
+
width: idWidth,
|
|
493
|
+
color: colors.textDim,
|
|
494
|
+
dimColor: false,
|
|
495
|
+
bold: false,
|
|
496
|
+
}),
|
|
419
497
|
{
|
|
420
|
-
key:
|
|
421
|
-
label:
|
|
498
|
+
key: "statusText",
|
|
499
|
+
label: "Status",
|
|
422
500
|
width: statusTextWidth,
|
|
423
501
|
render: (devbox, index, isSelected) => {
|
|
424
502
|
const statusDisplay = getStatusDisplay(devbox.status);
|
|
425
503
|
const truncated = statusDisplay.text.slice(0, statusTextWidth);
|
|
426
|
-
const padded = truncated.padEnd(statusTextWidth,
|
|
427
|
-
return (_jsx(Text, { color: isSelected ?
|
|
428
|
-
}
|
|
504
|
+
const padded = truncated.padEnd(statusTextWidth, " ");
|
|
505
|
+
return (_jsx(Text, { color: isSelected ? "white" : statusDisplay.color, bold: true, inverse: isSelected, wrap: "truncate", children: padded }));
|
|
506
|
+
},
|
|
429
507
|
},
|
|
430
|
-
createTextColumn(
|
|
431
|
-
|
|
432
|
-
|
|
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;
|
|
433
516
|
return hasCapabilities
|
|
434
517
|
? `[${devbox.capabilities
|
|
435
|
-
.filter((c) => c !==
|
|
436
|
-
.map((c) => c ===
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
+
}),
|
|
442
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) }) })] }));
|
|
443
555
|
}
|
|
444
|
-
// If loading or error, show that first
|
|
445
|
-
if (
|
|
446
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
447
|
-
{ label: 'Devboxes', active: true }
|
|
448
|
-
] }), _jsx(SpinnerComponent, { message: "Loading..." })] }));
|
|
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..." })] }));
|
|
449
559
|
}
|
|
450
560
|
if (error) {
|
|
451
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
452
|
-
{ label: 'Devboxes', active: true }
|
|
453
|
-
] }), _jsx(ErrorMessage, { message: "Failed to list devboxes", error: error })] }));
|
|
454
|
-
}
|
|
455
|
-
if (!loading && !error && devboxes.length === 0) {
|
|
456
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
457
|
-
{ label: 'Devboxes', active: true }
|
|
458
|
-
] }), _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" })] })] }));
|
|
561
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes", active: true }], showVersionCheck: true }), _jsx(ErrorMessage, { message: "Failed to list devboxes", error: error })] }));
|
|
459
562
|
}
|
|
460
|
-
// List view with data
|
|
461
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
462
|
-
{ label: 'Devboxes', active: true }
|
|
463
|
-
] }), currentDevboxes && currentDevboxes.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: () => {
|
|
563
|
+
// List view with data (always show, even if empty)
|
|
564
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes", active: true }] }), currentDevboxes && currentDevboxes.length >= 0 && (_jsxs(_Fragment, { children: [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 (name, id, status)...", onSubmit: () => {
|
|
464
565
|
setSearchMode(false);
|
|
465
566
|
setCurrentPage(0);
|
|
466
567
|
setSelectedIndex(0);
|
|
467
|
-
} }),
|
|
568
|
+
} }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[Esc to cancel]"] })] })), !searchMode && searchQuery && (_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: colors.primary, children: [figures.info, " Searching for: "] }), _jsx(Text, { color: colors.warning, bold: true, children: searchQuery }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "(", totalCount, " results) [/ to edit, Esc to clear]"] })] })), _jsx(Table, { data: currentDevboxes, keyExtractor: (devbox) => devbox.id, selectedIndex: selectedIndex, title: `devboxes[${totalCount}]`, columns: [
|
|
468
569
|
{
|
|
469
|
-
key:
|
|
470
|
-
label:
|
|
570
|
+
key: "statusIcon",
|
|
571
|
+
label: "",
|
|
471
572
|
width: statusIconWidth,
|
|
472
573
|
render: (devbox, index, isSelected) => {
|
|
473
574
|
const statusDisplay = getStatusDisplay(devbox.status);
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
const padded = icon.padEnd(statusIconWidth, ' ');
|
|
477
|
-
return (_jsx(Text, { color: isSelected ? 'white' : statusDisplay.color, bold: true, inverse: isSelected, children: padded }));
|
|
478
|
-
}
|
|
575
|
+
return (_jsxs(Text, { color: isSelected ? "white" : statusDisplay.color, bold: true, inverse: isSelected, wrap: "truncate", children: [statusDisplay.icon, " "] }));
|
|
576
|
+
},
|
|
479
577
|
},
|
|
480
|
-
createTextColumn(
|
|
578
|
+
createTextColumn("id", "ID", (devbox) => devbox.id, {
|
|
579
|
+
width: idWidth,
|
|
580
|
+
color: colors.textDim,
|
|
581
|
+
dimColor: false,
|
|
582
|
+
bold: false,
|
|
583
|
+
}),
|
|
481
584
|
{
|
|
482
|
-
key:
|
|
483
|
-
label:
|
|
585
|
+
key: "statusText",
|
|
586
|
+
label: "Status",
|
|
484
587
|
width: statusTextWidth,
|
|
485
588
|
render: (devbox, index, isSelected) => {
|
|
486
589
|
const statusDisplay = getStatusDisplay(devbox.status);
|
|
487
590
|
const truncated = statusDisplay.text.slice(0, statusTextWidth);
|
|
488
|
-
const padded = truncated.padEnd(statusTextWidth,
|
|
489
|
-
return (_jsx(Text, { color: isSelected ?
|
|
490
|
-
}
|
|
591
|
+
const padded = truncated.padEnd(statusTextWidth, " ");
|
|
592
|
+
return (_jsx(Text, { color: isSelected ? "white" : statusDisplay.color, bold: true, inverse: isSelected, wrap: "truncate", children: padded }));
|
|
593
|
+
},
|
|
491
594
|
},
|
|
492
|
-
createTextColumn(
|
|
493
|
-
|
|
494
|
-
|
|
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;
|
|
495
602
|
return hasCapabilities
|
|
496
603
|
? `[${devbox.capabilities
|
|
497
|
-
.filter((c) => c !==
|
|
498
|
-
.map((c) => c ===
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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"] })] })] }))] }));
|
|
505
641
|
};
|
|
506
642
|
// Export the UI component for use in the main menu
|
|
507
643
|
export { ListDevboxesUI };
|
|
@@ -511,7 +647,9 @@ export async function listDevboxes(options, focusDevboxId) {
|
|
|
511
647
|
await executor.executeList(async () => {
|
|
512
648
|
const client = executor.getClient();
|
|
513
649
|
return executor.fetchFromIterator(client.devboxes.list(), {
|
|
514
|
-
filter: options.status
|
|
650
|
+
filter: options.status
|
|
651
|
+
? (devbox) => devbox.status === options.status
|
|
652
|
+
: undefined,
|
|
515
653
|
limit: DEFAULT_PAGE_SIZE,
|
|
516
654
|
});
|
|
517
655
|
}, () => (_jsx(ListDevboxesUI, { status: options.status, focusDevboxId: focusDevboxId, onSSHRequest: (config) => {
|
|
@@ -523,7 +661,7 @@ export async function listDevboxes(options, focusDevboxId) {
|
|
|
523
661
|
if (result.shouldRestart) {
|
|
524
662
|
console.clear();
|
|
525
663
|
console.log(`\nSSH session ended. Returning to CLI...\n`);
|
|
526
|
-
await new Promise(resolve => setTimeout(resolve, 500));
|
|
664
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
527
665
|
// Restart the list view with the devbox ID to focus on
|
|
528
666
|
await listDevboxes(options, result.returnToDevboxId);
|
|
529
667
|
}
|