@runloop/rl-cli 1.8.0 → 1.10.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 (66) hide show
  1. package/README.md +21 -7
  2. package/dist/cli.js +0 -0
  3. package/dist/commands/blueprint/delete.js +21 -0
  4. package/dist/commands/blueprint/list.js +226 -174
  5. package/dist/commands/blueprint/prune.js +13 -28
  6. package/dist/commands/devbox/create.js +41 -0
  7. package/dist/commands/devbox/list.js +142 -110
  8. package/dist/commands/devbox/rsync.js +69 -41
  9. package/dist/commands/devbox/scp.js +180 -39
  10. package/dist/commands/devbox/tunnel.js +4 -19
  11. package/dist/commands/gateway-config/create.js +53 -0
  12. package/dist/commands/gateway-config/delete.js +21 -0
  13. package/dist/commands/gateway-config/get.js +18 -0
  14. package/dist/commands/gateway-config/list.js +493 -0
  15. package/dist/commands/gateway-config/update.js +70 -0
  16. package/dist/commands/snapshot/list.js +11 -2
  17. package/dist/commands/snapshot/prune.js +265 -0
  18. package/dist/components/BenchmarkMenu.js +23 -3
  19. package/dist/components/DetailedInfoView.js +20 -0
  20. package/dist/components/DevboxActionsMenu.js +26 -62
  21. package/dist/components/DevboxCreatePage.js +763 -15
  22. package/dist/components/DevboxDetailPage.js +73 -24
  23. package/dist/components/GatewayConfigCreatePage.js +272 -0
  24. package/dist/components/LogsViewer.js +6 -40
  25. package/dist/components/ResourceDetailPage.js +143 -160
  26. package/dist/components/ResourceListView.js +3 -33
  27. package/dist/components/ResourcePicker.js +234 -0
  28. package/dist/components/SecretCreatePage.js +71 -27
  29. package/dist/components/SettingsMenu.js +12 -2
  30. package/dist/components/StateHistory.js +1 -20
  31. package/dist/components/StatusBadge.js +9 -2
  32. package/dist/components/StreamingLogsViewer.js +8 -42
  33. package/dist/components/form/FormTextInput.js +4 -2
  34. package/dist/components/resourceDetailTypes.js +18 -0
  35. package/dist/hooks/useInputHandler.js +103 -0
  36. package/dist/router/Router.js +79 -2
  37. package/dist/screens/BenchmarkDetailScreen.js +163 -0
  38. package/dist/screens/BenchmarkJobCreateScreen.js +524 -0
  39. package/dist/screens/BenchmarkJobDetailScreen.js +614 -0
  40. package/dist/screens/BenchmarkJobListScreen.js +479 -0
  41. package/dist/screens/BenchmarkListScreen.js +266 -0
  42. package/dist/screens/BenchmarkMenuScreen.js +6 -0
  43. package/dist/screens/BenchmarkRunDetailScreen.js +258 -22
  44. package/dist/screens/BenchmarkRunListScreen.js +21 -1
  45. package/dist/screens/BlueprintDetailScreen.js +5 -1
  46. package/dist/screens/DevboxCreateScreen.js +2 -2
  47. package/dist/screens/GatewayConfigDetailScreen.js +236 -0
  48. package/dist/screens/GatewayConfigListScreen.js +7 -0
  49. package/dist/screens/ScenarioRunDetailScreen.js +6 -0
  50. package/dist/screens/SecretDetailScreen.js +26 -2
  51. package/dist/screens/SettingsMenuScreen.js +3 -0
  52. package/dist/screens/SnapshotDetailScreen.js +6 -0
  53. package/dist/services/agentService.js +42 -0
  54. package/dist/services/benchmarkJobService.js +122 -0
  55. package/dist/services/benchmarkService.js +47 -0
  56. package/dist/services/gatewayConfigService.js +153 -0
  57. package/dist/services/scenarioService.js +34 -0
  58. package/dist/store/benchmarkJobStore.js +66 -0
  59. package/dist/store/benchmarkStore.js +63 -0
  60. package/dist/store/gatewayConfigStore.js +83 -0
  61. package/dist/utils/browser.js +22 -0
  62. package/dist/utils/clipboard.js +41 -0
  63. package/dist/utils/commands.js +105 -9
  64. package/dist/utils/gatewayConfigValidation.js +58 -0
  65. package/dist/utils/time.js +121 -0
  66. package/package.json +43 -43
@@ -0,0 +1,493 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import { Box, Text, useInput, useApp } from "ink";
4
+ import figures from "figures";
5
+ import { getClient } from "../../utils/client.js";
6
+ import { Header } from "../../components/Header.js";
7
+ import { SpinnerComponent } from "../../components/Spinner.js";
8
+ import { ErrorMessage } from "../../components/ErrorMessage.js";
9
+ import { SuccessMessage } from "../../components/SuccessMessage.js";
10
+ import { Breadcrumb } from "../../components/Breadcrumb.js";
11
+ import { NavigationTips } from "../../components/NavigationTips.js";
12
+ import { Table, createTextColumn } from "../../components/Table.js";
13
+ import { ActionsPopup } from "../../components/ActionsPopup.js";
14
+ import { formatTimeAgo } from "../../components/ResourceListView.js";
15
+ import { SearchBar } from "../../components/SearchBar.js";
16
+ import { output, outputError } from "../../utils/output.js";
17
+ import { colors } from "../../utils/theme.js";
18
+ import { useViewportHeight } from "../../hooks/useViewportHeight.js";
19
+ import { useExitOnCtrlC } from "../../hooks/useExitOnCtrlC.js";
20
+ import { useCursorPagination } from "../../hooks/useCursorPagination.js";
21
+ import { useListSearch } from "../../hooks/useListSearch.js";
22
+ import { useNavigation } from "../../store/navigationStore.js";
23
+ import { GatewayConfigCreatePage } from "../../components/GatewayConfigCreatePage.js";
24
+ import { ConfirmationPrompt } from "../../components/ConfirmationPrompt.js";
25
+ const DEFAULT_PAGE_SIZE = 10;
26
+ /**
27
+ * Get a display label for the auth mechanism type
28
+ */
29
+ function getAuthTypeLabel(authMechanism) {
30
+ if (authMechanism.type === "bearer") {
31
+ return "Bearer";
32
+ }
33
+ if (authMechanism.type === "header") {
34
+ return authMechanism.key ? `Header: ${authMechanism.key}` : "Header";
35
+ }
36
+ return authMechanism.type;
37
+ }
38
+ const ListGatewayConfigsUI = ({ onBack, onExit, }) => {
39
+ const { exit: inkExit } = useApp();
40
+ const { navigate } = useNavigation();
41
+ const [selectedIndex, setSelectedIndex] = React.useState(0);
42
+ const [showPopup, setShowPopup] = React.useState(false);
43
+ const [selectedOperation, setSelectedOperation] = React.useState(0);
44
+ const [selectedConfig, setSelectedConfig] = React.useState(null);
45
+ const [executingOperation, setExecutingOperation] = React.useState(null);
46
+ const [operationResult, setOperationResult] = React.useState(null);
47
+ const [operationError, setOperationError] = React.useState(null);
48
+ const [operationLoading, setOperationLoading] = React.useState(false);
49
+ const [showCreateConfig, setShowCreateConfig] = React.useState(false);
50
+ const [showEditConfig, setShowEditConfig] = React.useState(false);
51
+ const [editingConfig, setEditingConfig] = React.useState(null);
52
+ const [showDeleteConfirm, setShowDeleteConfirm] = React.useState(false);
53
+ // Search state
54
+ const search = useListSearch({
55
+ onSearchSubmit: () => setSelectedIndex(0),
56
+ onSearchClear: () => setSelectedIndex(0),
57
+ });
58
+ // Calculate overhead for viewport height
59
+ const overhead = 13 + search.getSearchOverhead();
60
+ const { viewportHeight, terminalWidth } = useViewportHeight({
61
+ overhead,
62
+ minHeight: 5,
63
+ });
64
+ const PAGE_SIZE = viewportHeight;
65
+ // All width constants
66
+ const fixedWidth = 6; // border + padding
67
+ const idWidth = 25;
68
+ const authWidth = 15;
69
+ const timeWidth = 20;
70
+ const showEndpoint = terminalWidth >= 100;
71
+ const endpointWidth = Math.max(20, terminalWidth >= 140 ? 40 : 25);
72
+ // Name width uses remaining space after fixed columns
73
+ const baseWidth = fixedWidth + idWidth + authWidth + timeWidth;
74
+ const optionalWidth = showEndpoint ? endpointWidth : 0;
75
+ const remainingWidth = terminalWidth - baseWidth - optionalWidth;
76
+ const nameWidth = Math.min(80, Math.max(15, remainingWidth));
77
+ // Fetch function for pagination hook
78
+ const fetchPage = React.useCallback(async (params) => {
79
+ const client = getClient();
80
+ const pageConfigs = [];
81
+ // Build query params
82
+ const queryParams = {
83
+ limit: params.limit,
84
+ };
85
+ if (params.startingAt) {
86
+ queryParams.starting_after = params.startingAt;
87
+ }
88
+ if (search.submittedSearchQuery) {
89
+ queryParams.name = search.submittedSearchQuery;
90
+ }
91
+ // Fetch ONE page only
92
+ const page = (await client.gatewayConfigs.list(queryParams));
93
+ // Extract data and create defensive copies
94
+ if (page.gateway_configs && Array.isArray(page.gateway_configs)) {
95
+ page.gateway_configs.forEach((g) => {
96
+ pageConfigs.push({
97
+ id: g.id,
98
+ name: g.name,
99
+ description: g.description,
100
+ endpoint: g.endpoint,
101
+ create_time_ms: g.create_time_ms,
102
+ auth_mechanism: {
103
+ type: g.auth_mechanism.type,
104
+ key: g.auth_mechanism.key,
105
+ },
106
+ account_id: g.account_id,
107
+ });
108
+ });
109
+ }
110
+ const result = {
111
+ items: pageConfigs,
112
+ hasMore: page.has_more || false,
113
+ totalCount: page.total_count || pageConfigs.length,
114
+ };
115
+ return result;
116
+ }, [search.submittedSearchQuery]);
117
+ // Use the shared pagination hook
118
+ const { items: configs, loading, navigating, error, currentPage, hasMore, hasPrev, totalCount, nextPage, prevPage, refresh, } = useCursorPagination({
119
+ fetchPage,
120
+ pageSize: PAGE_SIZE,
121
+ getItemId: (config) => config.id,
122
+ pollInterval: 5000,
123
+ pollingEnabled: !showPopup &&
124
+ !executingOperation &&
125
+ !showCreateConfig &&
126
+ !showEditConfig &&
127
+ !showDeleteConfirm &&
128
+ !search.searchMode,
129
+ deps: [PAGE_SIZE, search.submittedSearchQuery],
130
+ });
131
+ // Operations for a specific gateway config (shown in popup)
132
+ const operations = React.useMemo(() => [
133
+ {
134
+ key: "view_details",
135
+ label: "View Details",
136
+ color: colors.primary,
137
+ icon: figures.pointer,
138
+ },
139
+ {
140
+ key: "edit",
141
+ label: "Edit AI Gateway Config",
142
+ color: colors.warning,
143
+ icon: figures.pointer,
144
+ },
145
+ {
146
+ key: "delete",
147
+ label: "Delete AI Gateway Config",
148
+ color: colors.error,
149
+ icon: figures.cross,
150
+ },
151
+ ], []);
152
+ // Build columns
153
+ const columns = React.useMemo(() => [
154
+ createTextColumn("id", "ID", (config) => config.id, {
155
+ width: idWidth + 1,
156
+ color: colors.idColor,
157
+ dimColor: false,
158
+ bold: false,
159
+ }),
160
+ createTextColumn("name", "Name", (config) => config.name || "", {
161
+ width: nameWidth,
162
+ }),
163
+ ...(showEndpoint
164
+ ? [
165
+ createTextColumn("endpoint", "Endpoint", (config) => config.endpoint || "", {
166
+ width: endpointWidth,
167
+ color: colors.textDim,
168
+ dimColor: false,
169
+ bold: false,
170
+ }),
171
+ ]
172
+ : []),
173
+ createTextColumn("auth", "Auth", (config) => getAuthTypeLabel(config.auth_mechanism), {
174
+ width: authWidth,
175
+ color: colors.info,
176
+ dimColor: false,
177
+ bold: false,
178
+ }),
179
+ createTextColumn("created", "Created", (config) => config.create_time_ms ? formatTimeAgo(config.create_time_ms) : "", {
180
+ width: timeWidth,
181
+ color: colors.textDim,
182
+ dimColor: false,
183
+ bold: false,
184
+ }),
185
+ ], [idWidth, nameWidth, endpointWidth, authWidth, timeWidth, showEndpoint]);
186
+ // Handle Ctrl+C to exit
187
+ useExitOnCtrlC();
188
+ // Ensure selected index is within bounds
189
+ React.useEffect(() => {
190
+ if (configs.length > 0 && selectedIndex >= configs.length) {
191
+ setSelectedIndex(Math.max(0, configs.length - 1));
192
+ }
193
+ }, [configs.length, selectedIndex]);
194
+ const selectedConfigItem = configs[selectedIndex];
195
+ // Calculate pagination info for display
196
+ const totalPages = Math.max(1, Math.ceil(totalCount / PAGE_SIZE));
197
+ const startIndex = currentPage * PAGE_SIZE;
198
+ const endIndex = startIndex + configs.length;
199
+ const executeOperation = async (config, operationKey) => {
200
+ const client = getClient();
201
+ if (!config)
202
+ return;
203
+ try {
204
+ setOperationLoading(true);
205
+ switch (operationKey) {
206
+ case "delete":
207
+ await client.gatewayConfigs.delete(config.id);
208
+ setOperationResult(`AI gateway config "${config.name}" deleted successfully`);
209
+ break;
210
+ }
211
+ }
212
+ catch (err) {
213
+ setOperationError(err);
214
+ }
215
+ finally {
216
+ setOperationLoading(false);
217
+ }
218
+ };
219
+ useInput((input, key) => {
220
+ // Handle search mode input
221
+ if (search.searchMode) {
222
+ if (key.escape) {
223
+ search.cancelSearch();
224
+ }
225
+ return;
226
+ }
227
+ // Handle operation result display
228
+ if (operationResult || operationError) {
229
+ if (input === "q" || key.escape || key.return) {
230
+ const wasDelete = executingOperation === "delete";
231
+ const hadError = operationError !== null;
232
+ setOperationResult(null);
233
+ setOperationError(null);
234
+ setExecutingOperation(null);
235
+ setSelectedConfig(null);
236
+ // Refresh the list after delete to show updated data
237
+ if (wasDelete && !hadError) {
238
+ setTimeout(() => refresh(), 0);
239
+ }
240
+ }
241
+ return;
242
+ }
243
+ // Handle create config screen
244
+ if (showCreateConfig) {
245
+ return;
246
+ }
247
+ // Handle edit config screen
248
+ if (showEditConfig) {
249
+ return;
250
+ }
251
+ // Handle popup navigation
252
+ if (showPopup) {
253
+ if (key.upArrow && selectedOperation > 0) {
254
+ setSelectedOperation(selectedOperation - 1);
255
+ }
256
+ else if (key.downArrow && selectedOperation < operations.length - 1) {
257
+ setSelectedOperation(selectedOperation + 1);
258
+ }
259
+ else if (key.return) {
260
+ setShowPopup(false);
261
+ const operationKey = operations[selectedOperation].key;
262
+ if (operationKey === "create") {
263
+ setShowCreateConfig(true);
264
+ }
265
+ else if (operationKey === "view_details") {
266
+ navigate("gateway-config-detail", {
267
+ gatewayConfigId: selectedConfigItem.id,
268
+ });
269
+ }
270
+ else if (operationKey === "edit") {
271
+ // Show edit form
272
+ setEditingConfig(selectedConfigItem);
273
+ setShowEditConfig(true);
274
+ }
275
+ else if (operationKey === "delete") {
276
+ // Show delete confirmation
277
+ setSelectedConfig(selectedConfigItem);
278
+ setShowDeleteConfirm(true);
279
+ }
280
+ else {
281
+ setSelectedConfig(selectedConfigItem);
282
+ setExecutingOperation(operationKey);
283
+ // Execute immediately with values passed directly
284
+ executeOperation(selectedConfigItem, operationKey);
285
+ }
286
+ }
287
+ else if (input === "c") {
288
+ // Create hotkey
289
+ setShowPopup(false);
290
+ setShowCreateConfig(true);
291
+ }
292
+ else if (input === "v" && selectedConfigItem) {
293
+ // View details hotkey
294
+ setShowPopup(false);
295
+ navigate("gateway-config-detail", {
296
+ gatewayConfigId: selectedConfigItem.id,
297
+ });
298
+ }
299
+ else if (input === "e" && selectedConfigItem) {
300
+ // Edit hotkey
301
+ setShowPopup(false);
302
+ setEditingConfig(selectedConfigItem);
303
+ setShowEditConfig(true);
304
+ }
305
+ else if (key.escape || input === "q") {
306
+ setShowPopup(false);
307
+ setSelectedOperation(0);
308
+ }
309
+ else if (input === "d") {
310
+ // Delete hotkey - show confirmation
311
+ setShowPopup(false);
312
+ setSelectedConfig(selectedConfigItem);
313
+ setShowDeleteConfirm(true);
314
+ }
315
+ return;
316
+ }
317
+ const pageConfigs = configs.length;
318
+ // Handle list view navigation
319
+ if (key.upArrow && selectedIndex > 0) {
320
+ setSelectedIndex(selectedIndex - 1);
321
+ }
322
+ else if (key.downArrow && selectedIndex < pageConfigs - 1) {
323
+ setSelectedIndex(selectedIndex + 1);
324
+ }
325
+ else if ((input === "n" || key.rightArrow) &&
326
+ !loading &&
327
+ !navigating &&
328
+ hasMore) {
329
+ nextPage();
330
+ setSelectedIndex(0);
331
+ }
332
+ else if ((input === "p" || key.leftArrow) &&
333
+ !loading &&
334
+ !navigating &&
335
+ hasPrev) {
336
+ prevPage();
337
+ setSelectedIndex(0);
338
+ }
339
+ else if (key.return && selectedConfigItem) {
340
+ // Enter key navigates to detail view
341
+ navigate("gateway-config-detail", {
342
+ gatewayConfigId: selectedConfigItem.id,
343
+ });
344
+ }
345
+ else if (input === "a") {
346
+ setShowPopup(true);
347
+ setSelectedOperation(0);
348
+ }
349
+ else if (input === "c") {
350
+ // Create shortcut
351
+ setShowCreateConfig(true);
352
+ }
353
+ else if (input === "e" && selectedConfigItem) {
354
+ // Edit shortcut
355
+ setEditingConfig(selectedConfigItem);
356
+ setShowEditConfig(true);
357
+ }
358
+ else if (input === "/") {
359
+ search.enterSearchMode();
360
+ }
361
+ else if (key.escape) {
362
+ if (search.handleEscape()) {
363
+ return;
364
+ }
365
+ if (onBack) {
366
+ onBack();
367
+ }
368
+ else if (onExit) {
369
+ onExit();
370
+ }
371
+ else {
372
+ inkExit();
373
+ }
374
+ }
375
+ });
376
+ // Delete confirmation
377
+ if (showDeleteConfirm && selectedConfig) {
378
+ return (_jsx(ConfirmationPrompt, { title: "Delete AI Gateway Config", message: `Are you sure you want to delete "${selectedConfig.name}"?`, details: "This action cannot be undone. Any devboxes using this AI gateway config will no longer have access to it.", breadcrumbItems: [
379
+ { label: "AI Gateway Configs" },
380
+ { label: selectedConfig.name || selectedConfig.id },
381
+ { label: "Delete", active: true },
382
+ ], onConfirm: () => {
383
+ setShowDeleteConfirm(false);
384
+ setExecutingOperation("delete");
385
+ executeOperation(selectedConfig, "delete");
386
+ }, onCancel: () => {
387
+ setShowDeleteConfirm(false);
388
+ setSelectedConfig(null);
389
+ } }));
390
+ }
391
+ // Operation result display
392
+ if (operationResult || operationError) {
393
+ const operationLabel = operations.find((o) => o.key === executingOperation)?.label ||
394
+ "Operation";
395
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
396
+ { label: "AI Gateway Configs" },
397
+ {
398
+ label: selectedConfig?.name || selectedConfig?.id || "Config",
399
+ },
400
+ { label: operationLabel, active: true },
401
+ ] }), _jsx(Header, { title: "Operation Result" }), operationResult && _jsx(SuccessMessage, { message: operationResult }), operationError && (_jsx(ErrorMessage, { message: "Operation failed", error: operationError })), _jsx(NavigationTips, { tips: [{ key: "Enter/q/esc", label: "Continue" }] })] }));
402
+ }
403
+ // Operation loading state
404
+ if (operationLoading && selectedConfig) {
405
+ const operationLabel = operations.find((o) => o.key === executingOperation)?.label ||
406
+ "Operation";
407
+ const messages = {
408
+ delete: "Deleting AI gateway config...",
409
+ };
410
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
411
+ { label: "AI Gateway Configs" },
412
+ { label: selectedConfig.name || selectedConfig.id },
413
+ { label: operationLabel, active: true },
414
+ ] }), _jsx(Header, { title: "Executing Operation" }), _jsx(SpinnerComponent, { message: messages[executingOperation] || "Please wait..." })] }));
415
+ }
416
+ // Create config screen
417
+ if (showCreateConfig) {
418
+ return (_jsx(GatewayConfigCreatePage, { onBack: () => setShowCreateConfig(false), onCreate: (config) => {
419
+ setShowCreateConfig(false);
420
+ navigate("gateway-config-detail", { gatewayConfigId: config.id });
421
+ } }));
422
+ }
423
+ // Edit config screen
424
+ if (showEditConfig && editingConfig) {
425
+ return (_jsx(GatewayConfigCreatePage, { onBack: () => {
426
+ setShowEditConfig(false);
427
+ setEditingConfig(null);
428
+ }, onCreate: () => {
429
+ setShowEditConfig(false);
430
+ setEditingConfig(null);
431
+ // Refresh the list to show updated data
432
+ setTimeout(() => refresh(), 0);
433
+ }, initialConfig: editingConfig }));
434
+ }
435
+ // Loading state
436
+ if (loading && configs.length === 0) {
437
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "AI Gateway Configs", active: true }] }), _jsx(SpinnerComponent, { message: "Loading AI gateway configs..." })] }));
438
+ }
439
+ // Error state
440
+ if (error) {
441
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "AI Gateway Configs", active: true }] }), _jsx(ErrorMessage, { message: "Failed to list AI gateway configs", error: error })] }));
442
+ }
443
+ // Main list view
444
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "AI Gateway Configs", active: true }] }), _jsx(SearchBar, { searchMode: search.searchMode, searchQuery: search.searchQuery, submittedSearchQuery: search.submittedSearchQuery, resultCount: totalCount, onSearchChange: search.setSearchQuery, onSearchSubmit: search.submitSearch, placeholder: "Search AI gateway configs..." }), !showPopup && (_jsx(Table, { data: configs, keyExtractor: (config) => config.id, selectedIndex: selectedIndex, title: `gateway_configs[${totalCount}]`, columns: columns, emptyState: _jsxs(Text, { color: colors.textDim, children: [figures.info, " No AI gateway configs found. Press [c] to create one."] }) })), !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] }), search.submittedSearchQuery && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.warning, children: ["Filtered: \"", search.submittedSearchQuery, "\""] })] }))] })), showPopup && selectedConfigItem && (_jsx(Box, { marginTop: 2, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedConfigItem, operations: operations.map((op) => ({
445
+ key: op.key,
446
+ label: op.label,
447
+ color: op.color,
448
+ icon: op.icon,
449
+ shortcut: op.key === "create"
450
+ ? "c"
451
+ : op.key === "view_details"
452
+ ? "v"
453
+ : op.key === "edit"
454
+ ? "e"
455
+ : op.key === "delete"
456
+ ? "d"
457
+ : "",
458
+ })), selectedOperation: selectedOperation, onClose: () => setShowPopup(false) }) })), _jsx(NavigationTips, { showArrows: true, tips: [
459
+ {
460
+ icon: `${figures.arrowLeft}${figures.arrowRight}`,
461
+ label: "Page",
462
+ condition: hasMore || hasPrev,
463
+ },
464
+ { key: "Enter", label: "Details" },
465
+ { key: "c", label: "Create" },
466
+ { key: "e", label: "Edit" },
467
+ { key: "a", label: "Actions" },
468
+ { key: "/", label: "Search" },
469
+ { key: "Esc", label: "Back" },
470
+ ] })] }));
471
+ };
472
+ // Export the UI component for use in the main menu
473
+ export { ListGatewayConfigsUI };
474
+ export async function listGatewayConfigs(options = {}) {
475
+ try {
476
+ const client = getClient();
477
+ // Build query params
478
+ const queryParams = {
479
+ limit: DEFAULT_PAGE_SIZE,
480
+ };
481
+ if (options.name) {
482
+ queryParams.name = options.name;
483
+ }
484
+ // Fetch gateway configs
485
+ const page = (await client.gatewayConfigs.list(queryParams));
486
+ // Extract gateway configs array
487
+ const gatewayConfigs = page.gateway_configs || [];
488
+ output(gatewayConfigs, { format: options.output, defaultFormat: "json" });
489
+ }
490
+ catch (error) {
491
+ outputError("Failed to list gateway configs", error);
492
+ }
493
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Update gateway config command
3
+ */
4
+ import { getClient } from "../../utils/client.js";
5
+ import { output, outputError } from "../../utils/output.js";
6
+ import { validateGatewayConfig } from "../../utils/gatewayConfigValidation.js";
7
+ export async function updateGatewayConfig(options) {
8
+ try {
9
+ const client = getClient();
10
+ // Validate that at most one auth type is specified
11
+ if (options.bearerAuth && options.headerAuth) {
12
+ outputError("Cannot specify both --bearer-auth and --header-auth. Choose one.");
13
+ return;
14
+ }
15
+ // Determine auth type if specified
16
+ const authType = options.bearerAuth
17
+ ? "bearer"
18
+ : options.headerAuth
19
+ ? "header"
20
+ : undefined;
21
+ // Validate provided fields using shared validation
22
+ const validation = validateGatewayConfig({
23
+ name: options.name,
24
+ endpoint: options.endpoint,
25
+ authType,
26
+ authKey: options.headerAuth,
27
+ }, { requireName: false, requireEndpoint: false });
28
+ if (!validation.valid) {
29
+ outputError(validation.errors.join("\n"));
30
+ return;
31
+ }
32
+ const { sanitized } = validation;
33
+ // Build update params - only include fields that are provided
34
+ const updateParams = {};
35
+ if (sanitized.name) {
36
+ updateParams.name = sanitized.name;
37
+ }
38
+ if (sanitized.endpoint) {
39
+ updateParams.endpoint = sanitized.endpoint;
40
+ }
41
+ if (options.description !== undefined) {
42
+ updateParams.description = options.description.trim() || undefined;
43
+ }
44
+ // Handle auth mechanism update
45
+ if (sanitized.authType === "bearer") {
46
+ updateParams.auth_mechanism = { type: "bearer" };
47
+ }
48
+ else if (sanitized.authType === "header" && sanitized.authKey) {
49
+ updateParams.auth_mechanism = {
50
+ type: "header",
51
+ key: sanitized.authKey,
52
+ };
53
+ }
54
+ if (Object.keys(updateParams).length === 0) {
55
+ outputError("No update options provided. Use --name, --endpoint, --bearer-auth, --header-auth, or --description");
56
+ return;
57
+ }
58
+ const config = await client.gatewayConfigs.update(options.id, updateParams);
59
+ // Default: just output the ID for easy scripting
60
+ if (!options.output || options.output === "text") {
61
+ console.log(config.id);
62
+ }
63
+ else {
64
+ output(config, { format: options.output, defaultFormat: "json" });
65
+ }
66
+ }
67
+ catch (error) {
68
+ outputError("Failed to update gateway config", error);
69
+ }
70
+ }
@@ -436,8 +436,17 @@ export async function listSnapshots(options) {
436
436
  }
437
437
  // Fetch snapshots
438
438
  const page = (await client.devboxes.listDiskSnapshots(queryParams));
439
- // Extract snapshots array
440
- const snapshots = page.snapshots || [];
439
+ // Extract snapshots array and strip to plain objects to avoid
440
+ // camelCase aliases added by the API client library
441
+ const snapshots = (page.snapshots || []).map((s) => ({
442
+ id: s.id,
443
+ name: s.name ?? undefined,
444
+ create_time_ms: s.create_time_ms,
445
+ metadata: s.metadata,
446
+ source_devbox_id: s.source_devbox_id,
447
+ source_blueprint_id: s.source_blueprint_id ?? undefined,
448
+ commit_message: s.commit_message ?? undefined,
449
+ }));
441
450
  output(snapshots, { format: options.output, defaultFormat: "json" });
442
451
  }
443
452
  catch (error) {