@runloop/rl-cli 0.1.2 → 0.3.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.
Files changed (105) hide show
  1. package/README.md +54 -10
  2. package/dist/cli.js +79 -72
  3. package/dist/commands/auth.js +2 -2
  4. package/dist/commands/blueprint/create.js +31 -83
  5. package/dist/commands/blueprint/get.js +29 -34
  6. package/dist/commands/blueprint/list.js +278 -230
  7. package/dist/commands/blueprint/logs.js +133 -37
  8. package/dist/commands/config.js +118 -0
  9. package/dist/commands/devbox/create.js +120 -40
  10. package/dist/commands/devbox/delete.js +17 -33
  11. package/dist/commands/devbox/download.js +29 -43
  12. package/dist/commands/devbox/exec.js +22 -39
  13. package/dist/commands/devbox/execAsync.js +20 -37
  14. package/dist/commands/devbox/get.js +13 -35
  15. package/dist/commands/devbox/getAsync.js +12 -34
  16. package/dist/commands/devbox/list.js +241 -402
  17. package/dist/commands/devbox/logs.js +20 -38
  18. package/dist/commands/devbox/read.js +29 -43
  19. package/dist/commands/devbox/resume.js +13 -35
  20. package/dist/commands/devbox/rsync.js +26 -78
  21. package/dist/commands/devbox/scp.js +25 -79
  22. package/dist/commands/devbox/sendStdin.js +41 -0
  23. package/dist/commands/devbox/shutdown.js +13 -35
  24. package/dist/commands/devbox/ssh.js +46 -78
  25. package/dist/commands/devbox/suspend.js +13 -35
  26. package/dist/commands/devbox/tunnel.js +37 -88
  27. package/dist/commands/devbox/upload.js +28 -36
  28. package/dist/commands/devbox/write.js +29 -44
  29. package/dist/commands/mcp-http.js +6 -5
  30. package/dist/commands/mcp-install.js +12 -10
  31. package/dist/commands/mcp.js +5 -4
  32. package/dist/commands/menu.js +26 -67
  33. package/dist/commands/object/delete.js +12 -34
  34. package/dist/commands/object/download.js +26 -74
  35. package/dist/commands/object/get.js +12 -34
  36. package/dist/commands/object/list.js +15 -93
  37. package/dist/commands/object/upload.js +35 -96
  38. package/dist/commands/snapshot/create.js +23 -39
  39. package/dist/commands/snapshot/delete.js +17 -33
  40. package/dist/commands/snapshot/get.js +16 -0
  41. package/dist/commands/snapshot/list.js +309 -80
  42. package/dist/commands/snapshot/status.js +12 -34
  43. package/dist/components/ActionsPopup.js +64 -39
  44. package/dist/components/Banner.js +7 -1
  45. package/dist/components/Breadcrumb.js +11 -48
  46. package/dist/components/DevboxActionsMenu.js +117 -207
  47. package/dist/components/DevboxCreatePage.js +12 -7
  48. package/dist/components/DevboxDetailPage.js +76 -28
  49. package/dist/components/ErrorBoundary.js +29 -0
  50. package/dist/components/ErrorMessage.js +10 -2
  51. package/dist/components/Header.js +12 -4
  52. package/dist/components/InteractiveSpawn.js +104 -0
  53. package/dist/components/LogsViewer.js +169 -0
  54. package/dist/components/MainMenu.js +37 -33
  55. package/dist/components/MetadataDisplay.js +4 -4
  56. package/dist/components/OperationsMenu.js +1 -1
  57. package/dist/components/ResourceActionsMenu.js +4 -4
  58. package/dist/components/ResourceListView.js +46 -34
  59. package/dist/components/Spinner.js +7 -2
  60. package/dist/components/StatusBadge.js +1 -1
  61. package/dist/components/SuccessMessage.js +12 -2
  62. package/dist/components/Table.js +16 -6
  63. package/dist/components/UpdateNotification.js +56 -0
  64. package/dist/hooks/useCursorPagination.js +125 -85
  65. package/dist/hooks/useExitOnCtrlC.js +15 -0
  66. package/dist/hooks/useViewportHeight.js +47 -0
  67. package/dist/mcp/server-http.js +2 -1
  68. package/dist/mcp/server.js +71 -7
  69. package/dist/router/Router.js +70 -0
  70. package/dist/router/types.js +1 -0
  71. package/dist/screens/BlueprintListScreen.js +7 -0
  72. package/dist/screens/BlueprintLogsScreen.js +74 -0
  73. package/dist/screens/DevboxActionsScreen.js +25 -0
  74. package/dist/screens/DevboxCreateScreen.js +11 -0
  75. package/dist/screens/DevboxDetailScreen.js +60 -0
  76. package/dist/screens/DevboxListScreen.js +23 -0
  77. package/dist/screens/LogsSessionScreen.js +49 -0
  78. package/dist/screens/MenuScreen.js +23 -0
  79. package/dist/screens/SSHSessionScreen.js +55 -0
  80. package/dist/screens/SnapshotListScreen.js +7 -0
  81. package/dist/services/blueprintService.js +101 -0
  82. package/dist/services/devboxService.js +215 -0
  83. package/dist/services/snapshotService.js +81 -0
  84. package/dist/store/blueprintStore.js +89 -0
  85. package/dist/store/devboxStore.js +105 -0
  86. package/dist/store/index.js +7 -0
  87. package/dist/store/navigationStore.js +101 -0
  88. package/dist/store/snapshotStore.js +87 -0
  89. package/dist/utils/client.js +4 -2
  90. package/dist/utils/config.js +22 -111
  91. package/dist/utils/interactiveCommand.js +3 -2
  92. package/dist/utils/logFormatter.js +208 -0
  93. package/dist/utils/memoryMonitor.js +85 -0
  94. package/dist/utils/output.js +153 -61
  95. package/dist/utils/process.js +106 -0
  96. package/dist/utils/processUtils.js +135 -0
  97. package/dist/utils/screen.js +61 -0
  98. package/dist/utils/ssh.js +6 -3
  99. package/dist/utils/sshSession.js +5 -29
  100. package/dist/utils/terminalDetection.js +185 -0
  101. package/dist/utils/terminalSync.js +39 -0
  102. package/dist/utils/theme.js +162 -13
  103. package/dist/utils/versionCheck.js +53 -0
  104. package/dist/version.js +12 -0
  105. package/package.json +19 -17
@@ -1,6 +1,6 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
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, useStdout } from "ink";
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 { createExecutor } from "../../utils/CommandExecutor.js";
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 { runSSHSession, } from "../../utils/sshSession.js";
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, onSSHRequest, focusDevboxId, onBack, onExit }) => {
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 [totalCount, setTotalCount] = React.useState(0);
41
- const [hasMore, setHasMore] = React.useState(false);
42
- const pageCache = React.useRef(new Map());
43
- const lastIdCache = React.useRef(new Map());
44
- // Calculate responsive dimensions
45
- const terminalWidth = stdout?.columns || 120;
46
- const terminalHeight = stdout?.rows || 30;
47
- // Calculate dynamic page size based on terminal height
48
- // Account for: Banner (3-4 lines) + Breadcrumb (1) + Header (1) + Stats (2) + Help text (2) + Margins (2) + Header row (1) = ~12 lines
49
- const PAGE_SIZE = Math.max(5, terminalHeight - 12);
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
- // Define allOperations
97
- const allOperations = [
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
- // Check if we need to focus on a specific devbox after returning from SSH
294
+ ], []);
295
+ // Handle Ctrl+C to exit
296
+ useExitOnCtrlC();
297
+ // Ensure selected index is within bounds
163
298
  React.useEffect(() => {
164
- if (focusDevboxId && devboxes.length > 0 && !initialLoading) {
165
- // Find the devbox in the current page
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, 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]);
179
- React.useEffect(() => {
180
- const list = async (isInitialLoad = false, isBackgroundRefresh = false) => {
181
- try {
182
- // Set navigating flag at the start (but not for background refresh)
183
- if (!isBackgroundRefresh) {
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
- finally {
250
- if (!isBackgroundRefresh) {
251
- isNavigating.current = false;
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
- // 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
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
- }, 3000);
273
- return () => clearInterval(interval);
274
- }, [showDetails, showCreate, showActions, currentPage, searchQuery]);
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
- // Handle Ctrl+C to force exit
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 - let DevboxDetailPage handle it
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 - let DevboxCreatePage handle it
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 - let DevboxActionsMenu handle it
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
- !isNavigating.current &&
352
- currentPage < totalPages - 1) {
353
- setCurrentPage(currentPage + 1);
382
+ !loading &&
383
+ !navigating &&
384
+ hasMore) {
385
+ nextPage();
354
386
  setSelectedIndex(0);
355
387
  }
356
388
  else if ((input === "p" || key.leftArrow) &&
357
- !isNavigating.current &&
358
- currentPage > 0) {
359
- setCurrentPage(currentPage - 1);
389
+ !loading &&
390
+ !navigating &&
391
+ hasPrev) {
392
+ prevPage();
360
393
  setSelectedIndex(0);
361
394
  }
362
395
  else if (key.return) {
363
- console.clear();
364
- setShowDetails(true);
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 (searchQuery) {
400
- // Clear search when Esc is pressed and there's an active search
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: (devbox) => {
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, onSSHRequest: onSSHRequest }));
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), onSSHRequest: onSSHRequest }));
472
+ return (_jsx(DevboxDetailPage, { devbox: selectedDevbox, onBack: () => setShowDetails(false) }));
478
473
  }
479
- // Show popup with table in background
480
- if (showPopup && selectedDevbox) {
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: [
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 }], showVersionCheck: true }), _jsx(ErrorMessage, { message: "Failed to list devboxes", error: error })] }));
479
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes", active: true }] }), _jsx(ErrorMessage, { message: "Failed to list devboxes", error: error })] }));
562
480
  }
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: () => {
565
- setSearchMode(false);
566
- setCurrentPage(0);
567
- setSelectedIndex(0);
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: [
569
- {
570
- key: "statusIcon",
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, focusDevboxId) {
645
- const executor = createExecutor(options);
646
- let sshSessionConfig = null;
647
- await executor.executeList(async () => {
648
- const client = executor.getClient();
649
- return executor.fetchFromIterator(client.devboxes.list(), {
650
- filter: options.status
651
- ? (devbox) => devbox.status === options.status
652
- : undefined,
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
  }