@runloop/rl-cli 1.7.1 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -5
- package/dist/cli.js +0 -0
- package/dist/commands/blueprint/delete.js +21 -0
- package/dist/commands/blueprint/list.js +226 -174
- package/dist/commands/blueprint/prune.js +13 -28
- package/dist/commands/devbox/create.js +41 -0
- package/dist/commands/devbox/list.js +125 -109
- package/dist/commands/devbox/tunnel.js +4 -19
- package/dist/commands/gateway-config/create.js +44 -0
- package/dist/commands/gateway-config/delete.js +21 -0
- package/dist/commands/gateway-config/get.js +15 -0
- package/dist/commands/gateway-config/list.js +493 -0
- package/dist/commands/gateway-config/update.js +60 -0
- package/dist/commands/menu.js +2 -1
- package/dist/commands/secret/list.js +379 -4
- package/dist/commands/snapshot/list.js +11 -2
- package/dist/commands/snapshot/prune.js +265 -0
- package/dist/components/BenchmarkMenu.js +108 -0
- package/dist/components/DetailedInfoView.js +20 -0
- package/dist/components/DevboxActionsMenu.js +9 -61
- package/dist/components/DevboxCreatePage.js +531 -14
- package/dist/components/DevboxDetailPage.js +27 -22
- package/dist/components/GatewayConfigCreatePage.js +265 -0
- package/dist/components/LogsViewer.js +6 -40
- package/dist/components/MainMenu.js +63 -22
- package/dist/components/ResourceDetailPage.js +143 -160
- package/dist/components/ResourceListView.js +3 -33
- package/dist/components/ResourcePicker.js +220 -0
- package/dist/components/SecretCreatePage.js +183 -0
- package/dist/components/SettingsMenu.js +95 -0
- package/dist/components/StateHistory.js +1 -20
- package/dist/components/StatusBadge.js +80 -0
- package/dist/components/StreamingLogsViewer.js +8 -42
- package/dist/components/form/FormTextInput.js +4 -2
- package/dist/components/resourceDetailTypes.js +18 -0
- package/dist/hooks/useInputHandler.js +103 -0
- package/dist/router/Router.js +99 -2
- package/dist/screens/BenchmarkDetailScreen.js +163 -0
- package/dist/screens/BenchmarkJobCreateScreen.js +524 -0
- package/dist/screens/BenchmarkJobDetailScreen.js +614 -0
- package/dist/screens/BenchmarkJobListScreen.js +479 -0
- package/dist/screens/BenchmarkListScreen.js +266 -0
- package/dist/screens/BenchmarkMenuScreen.js +29 -0
- package/dist/screens/BenchmarkRunDetailScreen.js +425 -0
- package/dist/screens/BenchmarkRunListScreen.js +275 -0
- package/dist/screens/BlueprintDetailScreen.js +5 -1
- package/dist/screens/DevboxCreateScreen.js +2 -2
- package/dist/screens/GatewayConfigDetailScreen.js +236 -0
- package/dist/screens/GatewayConfigListScreen.js +7 -0
- package/dist/screens/MenuScreen.js +5 -2
- package/dist/screens/ScenarioRunDetailScreen.js +226 -0
- package/dist/screens/ScenarioRunListScreen.js +245 -0
- package/dist/screens/SecretCreateScreen.js +7 -0
- package/dist/screens/SecretDetailScreen.js +198 -0
- package/dist/screens/SecretListScreen.js +7 -0
- package/dist/screens/SettingsMenuScreen.js +26 -0
- package/dist/screens/SnapshotDetailScreen.js +6 -0
- package/dist/services/agentService.js +42 -0
- package/dist/services/benchmarkJobService.js +122 -0
- package/dist/services/benchmarkService.js +120 -0
- package/dist/services/gatewayConfigService.js +114 -0
- package/dist/services/scenarioService.js +34 -0
- package/dist/store/benchmarkJobStore.js +66 -0
- package/dist/store/benchmarkStore.js +183 -0
- package/dist/store/betaFeatureStore.js +47 -0
- package/dist/store/gatewayConfigStore.js +83 -0
- package/dist/store/index.js +1 -0
- package/dist/utils/browser.js +22 -0
- package/dist/utils/clipboard.js +41 -0
- package/dist/utils/commands.js +80 -0
- package/dist/utils/config.js +8 -0
- package/dist/utils/time.js +121 -0
- package/package.json +42 -43
|
@@ -1,9 +1,384 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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";
|
|
4
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";
|
|
5
16
|
import { output, outputError } from "../../utils/output.js";
|
|
6
|
-
|
|
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 { ConfirmationPrompt } from "../../components/ConfirmationPrompt.js";
|
|
24
|
+
const DEFAULT_PAGE_SIZE = 10;
|
|
25
|
+
const ListSecretsUI = ({ onBack, onExit, }) => {
|
|
26
|
+
const { exit: inkExit } = useApp();
|
|
27
|
+
const { navigate } = useNavigation();
|
|
28
|
+
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
|
29
|
+
const [showPopup, setShowPopup] = React.useState(false);
|
|
30
|
+
const [selectedOperation, setSelectedOperation] = React.useState(0);
|
|
31
|
+
const [selectedSecret, setSelectedSecret] = React.useState(null);
|
|
32
|
+
const [executingOperation, setExecutingOperation] = React.useState(null);
|
|
33
|
+
const [operationResult, setOperationResult] = React.useState(null);
|
|
34
|
+
const [operationError, setOperationError] = React.useState(null);
|
|
35
|
+
const [operationLoading, setOperationLoading] = React.useState(false);
|
|
36
|
+
const [showDeleteConfirm, setShowDeleteConfirm] = React.useState(false);
|
|
37
|
+
// Search state
|
|
38
|
+
const search = useListSearch({
|
|
39
|
+
onSearchSubmit: () => setSelectedIndex(0),
|
|
40
|
+
onSearchClear: () => setSelectedIndex(0),
|
|
41
|
+
});
|
|
42
|
+
// Calculate overhead for viewport height
|
|
43
|
+
const overhead = 13 + search.getSearchOverhead();
|
|
44
|
+
const { viewportHeight, terminalWidth } = useViewportHeight({
|
|
45
|
+
overhead,
|
|
46
|
+
minHeight: 5,
|
|
47
|
+
});
|
|
48
|
+
const PAGE_SIZE = viewportHeight;
|
|
49
|
+
// All width constants
|
|
50
|
+
const fixedWidth = 6; // border + padding
|
|
51
|
+
const idWidth = 30;
|
|
52
|
+
const timeWidth = 20;
|
|
53
|
+
// Name width uses remaining space after fixed columns
|
|
54
|
+
const baseWidth = fixedWidth + idWidth + timeWidth;
|
|
55
|
+
const remainingWidth = terminalWidth - baseWidth;
|
|
56
|
+
const nameWidth = Math.min(80, Math.max(15, remainingWidth));
|
|
57
|
+
// Fetch function for pagination hook
|
|
58
|
+
const fetchPage = React.useCallback(async (params) => {
|
|
59
|
+
const client = getClient();
|
|
60
|
+
const pageSecrets = [];
|
|
61
|
+
// Secrets API doesn't support cursor pagination, fetch all and paginate client-side
|
|
62
|
+
const result = await client.secrets.list({ limit: 5000 });
|
|
63
|
+
// Extract data and filter by search if needed
|
|
64
|
+
if (result.secrets && Array.isArray(result.secrets)) {
|
|
65
|
+
let filtered = result.secrets;
|
|
66
|
+
// Client-side search filtering
|
|
67
|
+
if (search.submittedSearchQuery) {
|
|
68
|
+
const query = search.submittedSearchQuery.toLowerCase();
|
|
69
|
+
filtered = filtered.filter((s) => s.name?.toLowerCase().includes(query) ||
|
|
70
|
+
s.id?.toLowerCase().includes(query));
|
|
71
|
+
}
|
|
72
|
+
// Client-side pagination
|
|
73
|
+
const startIdx = params.startingAt
|
|
74
|
+
? filtered.findIndex((s) => s.id === params.startingAt) + 1
|
|
75
|
+
: 0;
|
|
76
|
+
const pageItems = filtered.slice(startIdx, startIdx + params.limit);
|
|
77
|
+
pageItems.forEach((s) => {
|
|
78
|
+
pageSecrets.push({
|
|
79
|
+
id: s.id,
|
|
80
|
+
name: s.name,
|
|
81
|
+
create_time_ms: s.create_time_ms,
|
|
82
|
+
update_time_ms: s.update_time_ms,
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
return {
|
|
86
|
+
items: pageSecrets,
|
|
87
|
+
hasMore: startIdx + params.limit < filtered.length,
|
|
88
|
+
totalCount: filtered.length,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
items: pageSecrets,
|
|
93
|
+
hasMore: false,
|
|
94
|
+
totalCount: 0,
|
|
95
|
+
};
|
|
96
|
+
}, [search.submittedSearchQuery]);
|
|
97
|
+
// Use the shared pagination hook
|
|
98
|
+
const { items: secrets, loading, navigating, error, currentPage, hasMore, hasPrev, totalCount, nextPage, prevPage, refresh, } = useCursorPagination({
|
|
99
|
+
fetchPage,
|
|
100
|
+
pageSize: PAGE_SIZE,
|
|
101
|
+
getItemId: (secret) => secret.id,
|
|
102
|
+
pollInterval: 10000,
|
|
103
|
+
pollingEnabled: !showPopup &&
|
|
104
|
+
!executingOperation &&
|
|
105
|
+
!showDeleteConfirm &&
|
|
106
|
+
!search.searchMode,
|
|
107
|
+
deps: [PAGE_SIZE, search.submittedSearchQuery],
|
|
108
|
+
});
|
|
109
|
+
// Operations for a specific secret (shown in popup)
|
|
110
|
+
const operations = React.useMemo(() => [
|
|
111
|
+
{
|
|
112
|
+
key: "view_details",
|
|
113
|
+
label: "View Details",
|
|
114
|
+
color: colors.primary,
|
|
115
|
+
icon: figures.pointer,
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
key: "delete",
|
|
119
|
+
label: "Delete Secret",
|
|
120
|
+
color: colors.error,
|
|
121
|
+
icon: figures.cross,
|
|
122
|
+
},
|
|
123
|
+
], []);
|
|
124
|
+
// Build columns
|
|
125
|
+
const columns = React.useMemo(() => [
|
|
126
|
+
createTextColumn("id", "ID", (secret) => secret.id, {
|
|
127
|
+
width: idWidth + 1,
|
|
128
|
+
color: colors.idColor,
|
|
129
|
+
dimColor: false,
|
|
130
|
+
bold: false,
|
|
131
|
+
}),
|
|
132
|
+
createTextColumn("name", "Name", (secret) => secret.name || "", {
|
|
133
|
+
width: nameWidth,
|
|
134
|
+
}),
|
|
135
|
+
createTextColumn("created", "Created", (secret) => secret.create_time_ms ? formatTimeAgo(secret.create_time_ms) : "-", {
|
|
136
|
+
width: timeWidth,
|
|
137
|
+
color: colors.textDim,
|
|
138
|
+
dimColor: false,
|
|
139
|
+
bold: false,
|
|
140
|
+
}),
|
|
141
|
+
], [idWidth, nameWidth, timeWidth]);
|
|
142
|
+
// Handle Ctrl+C to exit
|
|
143
|
+
useExitOnCtrlC();
|
|
144
|
+
// Ensure selected index is within bounds
|
|
145
|
+
React.useEffect(() => {
|
|
146
|
+
if (secrets.length > 0 && selectedIndex >= secrets.length) {
|
|
147
|
+
setSelectedIndex(Math.max(0, secrets.length - 1));
|
|
148
|
+
}
|
|
149
|
+
}, [secrets.length, selectedIndex]);
|
|
150
|
+
const selectedSecretItem = secrets[selectedIndex];
|
|
151
|
+
// Calculate pagination info for display
|
|
152
|
+
const totalPages = Math.max(1, Math.ceil(totalCount / PAGE_SIZE));
|
|
153
|
+
const startIndex = currentPage * PAGE_SIZE;
|
|
154
|
+
const endIndex = startIndex + secrets.length;
|
|
155
|
+
const executeOperation = async (secret, operationKey) => {
|
|
156
|
+
const client = getClient();
|
|
157
|
+
if (!secret)
|
|
158
|
+
return;
|
|
159
|
+
try {
|
|
160
|
+
setOperationLoading(true);
|
|
161
|
+
switch (operationKey) {
|
|
162
|
+
case "delete":
|
|
163
|
+
await client.secrets.delete(secret.name);
|
|
164
|
+
setOperationResult(`Secret "${secret.name}" deleted successfully`);
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
setOperationError(err);
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
setOperationLoading(false);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
useInput((input, key) => {
|
|
176
|
+
// Handle search mode input
|
|
177
|
+
if (search.searchMode) {
|
|
178
|
+
if (key.escape) {
|
|
179
|
+
search.cancelSearch();
|
|
180
|
+
}
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
// Handle operation result display
|
|
184
|
+
if (operationResult || operationError) {
|
|
185
|
+
if (input === "q" || key.escape || key.return) {
|
|
186
|
+
const wasDelete = executingOperation === "delete";
|
|
187
|
+
const hadError = operationError !== null;
|
|
188
|
+
setOperationResult(null);
|
|
189
|
+
setOperationError(null);
|
|
190
|
+
setExecutingOperation(null);
|
|
191
|
+
setSelectedSecret(null);
|
|
192
|
+
// Refresh the list after delete to show updated data
|
|
193
|
+
if (wasDelete && !hadError) {
|
|
194
|
+
setTimeout(() => refresh(), 0);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
// Handle popup navigation
|
|
200
|
+
if (showPopup) {
|
|
201
|
+
if (key.upArrow && selectedOperation > 0) {
|
|
202
|
+
setSelectedOperation(selectedOperation - 1);
|
|
203
|
+
}
|
|
204
|
+
else if (key.downArrow && selectedOperation < operations.length - 1) {
|
|
205
|
+
setSelectedOperation(selectedOperation + 1);
|
|
206
|
+
}
|
|
207
|
+
else if (key.return) {
|
|
208
|
+
setShowPopup(false);
|
|
209
|
+
const operationKey = operations[selectedOperation].key;
|
|
210
|
+
if (operationKey === "view_details") {
|
|
211
|
+
navigate("secret-detail", {
|
|
212
|
+
secretId: selectedSecretItem.id,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
else if (operationKey === "delete") {
|
|
216
|
+
// Show delete confirmation
|
|
217
|
+
setSelectedSecret(selectedSecretItem);
|
|
218
|
+
setShowDeleteConfirm(true);
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
setSelectedSecret(selectedSecretItem);
|
|
222
|
+
setExecutingOperation(operationKey);
|
|
223
|
+
// Execute immediately with values passed directly
|
|
224
|
+
executeOperation(selectedSecretItem, operationKey);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
else if (input === "c") {
|
|
228
|
+
// Create hotkey
|
|
229
|
+
setShowPopup(false);
|
|
230
|
+
navigate("secret-create");
|
|
231
|
+
}
|
|
232
|
+
else if (input === "v" && selectedSecretItem) {
|
|
233
|
+
// View details hotkey
|
|
234
|
+
setShowPopup(false);
|
|
235
|
+
navigate("secret-detail", {
|
|
236
|
+
secretId: selectedSecretItem.id,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
else if (key.escape || input === "q") {
|
|
240
|
+
setShowPopup(false);
|
|
241
|
+
setSelectedOperation(0);
|
|
242
|
+
}
|
|
243
|
+
else if (input === "d") {
|
|
244
|
+
// Delete hotkey - show confirmation
|
|
245
|
+
setShowPopup(false);
|
|
246
|
+
setSelectedSecret(selectedSecretItem);
|
|
247
|
+
setShowDeleteConfirm(true);
|
|
248
|
+
}
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const pageSecrets = secrets.length;
|
|
252
|
+
// Handle list view navigation
|
|
253
|
+
if (key.upArrow && selectedIndex > 0) {
|
|
254
|
+
setSelectedIndex(selectedIndex - 1);
|
|
255
|
+
}
|
|
256
|
+
else if (key.downArrow && selectedIndex < pageSecrets - 1) {
|
|
257
|
+
setSelectedIndex(selectedIndex + 1);
|
|
258
|
+
}
|
|
259
|
+
else if ((input === "n" || key.rightArrow) &&
|
|
260
|
+
!loading &&
|
|
261
|
+
!navigating &&
|
|
262
|
+
hasMore) {
|
|
263
|
+
nextPage();
|
|
264
|
+
setSelectedIndex(0);
|
|
265
|
+
}
|
|
266
|
+
else if ((input === "p" || key.leftArrow) &&
|
|
267
|
+
!loading &&
|
|
268
|
+
!navigating &&
|
|
269
|
+
hasPrev) {
|
|
270
|
+
prevPage();
|
|
271
|
+
setSelectedIndex(0);
|
|
272
|
+
}
|
|
273
|
+
else if (key.return && selectedSecretItem) {
|
|
274
|
+
// Enter key navigates to detail view
|
|
275
|
+
navigate("secret-detail", {
|
|
276
|
+
secretId: selectedSecretItem.id,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
else if (input === "a") {
|
|
280
|
+
setShowPopup(true);
|
|
281
|
+
setSelectedOperation(0);
|
|
282
|
+
}
|
|
283
|
+
else if (input === "c") {
|
|
284
|
+
// Create shortcut
|
|
285
|
+
navigate("secret-create");
|
|
286
|
+
}
|
|
287
|
+
else if (input === "/") {
|
|
288
|
+
search.enterSearchMode();
|
|
289
|
+
}
|
|
290
|
+
else if (key.escape) {
|
|
291
|
+
if (search.handleEscape()) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (onBack) {
|
|
295
|
+
onBack();
|
|
296
|
+
}
|
|
297
|
+
else if (onExit) {
|
|
298
|
+
onExit();
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
inkExit();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
// Delete confirmation
|
|
306
|
+
if (showDeleteConfirm && selectedSecret) {
|
|
307
|
+
return (_jsx(ConfirmationPrompt, { title: "Delete Secret", message: `Are you sure you want to delete "${selectedSecret.name}"?`, details: "This action cannot be undone. Any devboxes using this secret will no longer have access to it.", breadcrumbItems: [
|
|
308
|
+
{ label: "Settings" },
|
|
309
|
+
{ label: "Secrets" },
|
|
310
|
+
{ label: selectedSecret.name || selectedSecret.id },
|
|
311
|
+
{ label: "Delete", active: true },
|
|
312
|
+
], onConfirm: () => {
|
|
313
|
+
setShowDeleteConfirm(false);
|
|
314
|
+
setExecutingOperation("delete");
|
|
315
|
+
executeOperation(selectedSecret, "delete");
|
|
316
|
+
}, onCancel: () => {
|
|
317
|
+
setShowDeleteConfirm(false);
|
|
318
|
+
setSelectedSecret(null);
|
|
319
|
+
} }));
|
|
320
|
+
}
|
|
321
|
+
// Operation result display
|
|
322
|
+
if (operationResult || operationError) {
|
|
323
|
+
const operationLabel = operations.find((o) => o.key === executingOperation)?.label ||
|
|
324
|
+
"Operation";
|
|
325
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
326
|
+
{ label: "Settings" },
|
|
327
|
+
{ label: "Secrets" },
|
|
328
|
+
{
|
|
329
|
+
label: selectedSecret?.name || selectedSecret?.id || "Secret",
|
|
330
|
+
},
|
|
331
|
+
{ label: operationLabel, active: true },
|
|
332
|
+
] }), _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" }] })] }));
|
|
333
|
+
}
|
|
334
|
+
// Operation loading state
|
|
335
|
+
if (operationLoading && selectedSecret) {
|
|
336
|
+
const operationLabel = operations.find((o) => o.key === executingOperation)?.label ||
|
|
337
|
+
"Operation";
|
|
338
|
+
const messages = {
|
|
339
|
+
delete: "Deleting secret...",
|
|
340
|
+
};
|
|
341
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
342
|
+
{ label: "Settings" },
|
|
343
|
+
{ label: "Secrets" },
|
|
344
|
+
{ label: selectedSecret.name || selectedSecret.id },
|
|
345
|
+
{ label: operationLabel, active: true },
|
|
346
|
+
] }), _jsx(Header, { title: "Executing Operation" }), _jsx(SpinnerComponent, { message: messages[executingOperation] || "Please wait..." })] }));
|
|
347
|
+
}
|
|
348
|
+
// Loading state
|
|
349
|
+
if (loading && secrets.length === 0) {
|
|
350
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Settings" }, { label: "Secrets", active: true }] }), _jsx(SpinnerComponent, { message: "Loading secrets..." })] }));
|
|
351
|
+
}
|
|
352
|
+
// Error state
|
|
353
|
+
if (error) {
|
|
354
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Settings" }, { label: "Secrets", active: true }] }), _jsx(ErrorMessage, { message: "Failed to list secrets", error: error })] }));
|
|
355
|
+
}
|
|
356
|
+
// Main list view
|
|
357
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Settings" }, { label: "Secrets", active: true }] }), _jsx(SearchBar, { searchMode: search.searchMode, searchQuery: search.searchQuery, submittedSearchQuery: search.submittedSearchQuery, resultCount: totalCount, onSearchChange: search.setSearchQuery, onSearchSubmit: search.submitSearch, placeholder: "Search secrets..." }), !showPopup && (_jsx(Table, { data: secrets, keyExtractor: (secret) => secret.id, selectedIndex: selectedIndex, title: `secrets[${totalCount}]`, columns: columns, emptyState: _jsxs(Text, { color: colors.textDim, children: [figures.info, " No secrets 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 && selectedSecretItem && (_jsx(Box, { marginTop: 2, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedSecretItem, operations: operations.map((op) => ({
|
|
358
|
+
key: op.key,
|
|
359
|
+
label: op.label,
|
|
360
|
+
color: op.color,
|
|
361
|
+
icon: op.icon,
|
|
362
|
+
shortcut: op.key === "view_details"
|
|
363
|
+
? "v"
|
|
364
|
+
: op.key === "delete"
|
|
365
|
+
? "d"
|
|
366
|
+
: "",
|
|
367
|
+
})), selectedOperation: selectedOperation, onClose: () => setShowPopup(false) }) })), _jsx(NavigationTips, { showArrows: true, tips: [
|
|
368
|
+
{
|
|
369
|
+
icon: `${figures.arrowLeft}${figures.arrowRight}`,
|
|
370
|
+
label: "Page",
|
|
371
|
+
condition: hasMore || hasPrev,
|
|
372
|
+
},
|
|
373
|
+
{ key: "Enter", label: "Details" },
|
|
374
|
+
{ key: "c", label: "Create" },
|
|
375
|
+
{ key: "a", label: "Actions" },
|
|
376
|
+
{ key: "/", label: "Search" },
|
|
377
|
+
{ key: "Esc", label: "Back" },
|
|
378
|
+
] })] }));
|
|
379
|
+
};
|
|
380
|
+
// Export the UI component for use in the main menu
|
|
381
|
+
export { ListSecretsUI };
|
|
7
382
|
export async function listSecrets(options = {}) {
|
|
8
383
|
try {
|
|
9
384
|
const client = getClient();
|
|
@@ -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
|
-
|
|
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) {
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snapshot prune command - Delete old snapshots for a given source devbox
|
|
3
|
+
*/
|
|
4
|
+
import * as readline from "readline";
|
|
5
|
+
import { getClient } from "../../utils/client.js";
|
|
6
|
+
import { output, outputError } from "../../utils/output.js";
|
|
7
|
+
import { formatRelativeTime } from "../../utils/time.js";
|
|
8
|
+
/**
|
|
9
|
+
* Query the async status for a snapshot and return a normalized status string.
|
|
10
|
+
* Maps API statuses: "complete" → "ready", others passed through.
|
|
11
|
+
*/
|
|
12
|
+
async function querySnapshotStatus(snapshotId) {
|
|
13
|
+
const client = getClient();
|
|
14
|
+
try {
|
|
15
|
+
const statusResponse = await client.devboxes.diskSnapshots.queryStatus(snapshotId);
|
|
16
|
+
const operationStatus = statusResponse.status;
|
|
17
|
+
return operationStatus === "complete" ? "ready" : operationStatus;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return "unknown";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Fetch all snapshots for a given source devbox (handles pagination)
|
|
25
|
+
* and enrich each snapshot with its async operation status.
|
|
26
|
+
*/
|
|
27
|
+
async function fetchAllSnapshotsForDevbox(devboxId) {
|
|
28
|
+
const client = getClient();
|
|
29
|
+
const allSnapshots = [];
|
|
30
|
+
let hasMore = true;
|
|
31
|
+
let startingAfter = undefined;
|
|
32
|
+
while (hasMore) {
|
|
33
|
+
const params = {
|
|
34
|
+
devbox_id: devboxId,
|
|
35
|
+
limit: 100,
|
|
36
|
+
};
|
|
37
|
+
if (startingAfter) {
|
|
38
|
+
params.starting_after = startingAfter;
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const page = await client.devboxes.listDiskSnapshots(params);
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
43
|
+
const snapshots = (page.snapshots || []);
|
|
44
|
+
allSnapshots.push(...snapshots);
|
|
45
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
46
|
+
hasMore = page.has_more || false;
|
|
47
|
+
if (hasMore && snapshots.length > 0) {
|
|
48
|
+
startingAfter = snapshots[snapshots.length - 1].id;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
hasMore = false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
console.error("Warning: Error fetching snapshots:", error);
|
|
56
|
+
// Continue with partial results
|
|
57
|
+
hasMore = false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// The listDiskSnapshots endpoint does not include status — query it for each snapshot
|
|
61
|
+
const enriched = await Promise.all(allSnapshots.map(async (snapshot) => ({
|
|
62
|
+
...snapshot,
|
|
63
|
+
status: await querySnapshotStatus(snapshot.id),
|
|
64
|
+
})));
|
|
65
|
+
return enriched;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Categorize snapshots into successful and failed, and determine what to keep/delete
|
|
69
|
+
*/
|
|
70
|
+
function categorizeSnapshots(snapshots, keepCount) {
|
|
71
|
+
// Filter successful snapshots (status "ready" means completed successfully)
|
|
72
|
+
const successful = snapshots.filter((s) => s.status === "ready");
|
|
73
|
+
// Filter failed/incomplete snapshots
|
|
74
|
+
const failed = snapshots.filter((s) => s.status !== "ready");
|
|
75
|
+
// Sort successful by create_time_ms descending (newest first)
|
|
76
|
+
successful.sort((a, b) => (b.create_time_ms || 0) - (a.create_time_ms || 0));
|
|
77
|
+
// Determine what to keep and delete
|
|
78
|
+
const toKeep = successful.slice(0, keepCount);
|
|
79
|
+
const toDelete = [...successful.slice(keepCount), ...failed];
|
|
80
|
+
return {
|
|
81
|
+
toKeep,
|
|
82
|
+
toDelete,
|
|
83
|
+
successful,
|
|
84
|
+
failed,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Display a summary of what will be kept and deleted
|
|
89
|
+
*/
|
|
90
|
+
function displaySummary(devboxId, result, isDryRun) {
|
|
91
|
+
const total = result.successful.length + result.failed.length;
|
|
92
|
+
console.log(`\nAnalyzing snapshots for devbox "${devboxId}"...`);
|
|
93
|
+
console.log(`\nFound ${total} snapshot${total !== 1 ? "s" : ""}:`);
|
|
94
|
+
console.log(` ✓ ${result.successful.length} ready snapshot${result.successful.length !== 1 ? "s" : ""}`);
|
|
95
|
+
console.log(` ✗ ${result.failed.length} failed/incomplete snapshot${result.failed.length !== 1 ? "s" : ""}`);
|
|
96
|
+
// Show what will be kept
|
|
97
|
+
console.log(`\nKeeping (${result.toKeep.length} most recent ready):`);
|
|
98
|
+
if (result.toKeep.length === 0) {
|
|
99
|
+
console.log(" (none - no ready snapshots found)");
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
for (const snapshot of result.toKeep) {
|
|
103
|
+
const label = snapshot.name ? ` "${snapshot.name}"` : "";
|
|
104
|
+
console.log(` ✓ ${snapshot.id}${label} - Created ${formatRelativeTime(snapshot.create_time_ms)}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Show what will be deleted
|
|
108
|
+
console.log(`\n${isDryRun ? "Would delete" : "To be deleted"} (${result.toDelete.length} snapshot${result.toDelete.length !== 1 ? "s" : ""}):`);
|
|
109
|
+
if (result.toDelete.length === 0) {
|
|
110
|
+
console.log(" (none)");
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
for (const snapshot of result.toDelete) {
|
|
114
|
+
const icon = snapshot.status === "ready" ? "✓" : "⚠";
|
|
115
|
+
const statusLabel = snapshot.status === "ready" ? "ready" : snapshot.status || "unknown";
|
|
116
|
+
const label = snapshot.name ? ` "${snapshot.name}"` : "";
|
|
117
|
+
console.log(` ${icon} ${snapshot.id}${label} - Created ${formatRelativeTime(snapshot.create_time_ms)} (${statusLabel})`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Display all deleted snapshots
|
|
123
|
+
*/
|
|
124
|
+
function displayDeletedSnapshots(deleted) {
|
|
125
|
+
if (deleted.length === 0) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
console.log("\nDeleted snapshots:");
|
|
129
|
+
for (const snapshot of deleted) {
|
|
130
|
+
const icon = snapshot.status === "ready" ? "✓" : "⚠";
|
|
131
|
+
const statusLabel = snapshot.status === "ready" ? "ready" : snapshot.status || "unknown";
|
|
132
|
+
const label = snapshot.name ? ` "${snapshot.name}"` : "";
|
|
133
|
+
console.log(` ${icon} ${snapshot.id}${label} - Created ${formatRelativeTime(snapshot.create_time_ms)} (${statusLabel})`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Prompt user for confirmation
|
|
138
|
+
*/
|
|
139
|
+
async function confirmDeletion(count) {
|
|
140
|
+
const rl = readline.createInterface({
|
|
141
|
+
input: process.stdin,
|
|
142
|
+
output: process.stdout,
|
|
143
|
+
});
|
|
144
|
+
return new Promise((resolve) => {
|
|
145
|
+
rl.question(`\nDelete ${count} snapshot${count !== 1 ? "s" : ""}? (y/N): `, (answer) => {
|
|
146
|
+
rl.close();
|
|
147
|
+
resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Delete snapshots with error tracking
|
|
153
|
+
*/
|
|
154
|
+
async function deleteSnapshotsWithTracking(snapshots) {
|
|
155
|
+
const client = getClient();
|
|
156
|
+
const results = {
|
|
157
|
+
deleted: [],
|
|
158
|
+
failed: [],
|
|
159
|
+
};
|
|
160
|
+
for (const snapshot of snapshots) {
|
|
161
|
+
try {
|
|
162
|
+
await client.devboxes.diskSnapshots.delete(snapshot.id);
|
|
163
|
+
results.deleted.push(snapshot);
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
results.failed.push({
|
|
167
|
+
id: snapshot.id,
|
|
168
|
+
error: error instanceof Error ? error.message : String(error),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return results;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Main prune function
|
|
176
|
+
*/
|
|
177
|
+
export async function pruneSnapshots(devboxId, options = {}) {
|
|
178
|
+
try {
|
|
179
|
+
// Parse and validate options
|
|
180
|
+
const isDryRun = !!options.dryRun;
|
|
181
|
+
const autoConfirm = !!options.yes;
|
|
182
|
+
const keepCount = parseInt(options.keep || "1", 10);
|
|
183
|
+
if (isNaN(keepCount) || keepCount < 0) {
|
|
184
|
+
outputError("--keep must be a non-negative integer");
|
|
185
|
+
}
|
|
186
|
+
// Fetch all snapshots for the given devbox
|
|
187
|
+
console.log(`Fetching snapshots for devbox "${devboxId}"...`);
|
|
188
|
+
const snapshots = await fetchAllSnapshotsForDevbox(devboxId);
|
|
189
|
+
// Handle no snapshots found
|
|
190
|
+
if (snapshots.length === 0) {
|
|
191
|
+
console.log(`No snapshots found for devbox: ${devboxId}`);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
// Categorize snapshots
|
|
195
|
+
const categorized = categorizeSnapshots(snapshots, keepCount);
|
|
196
|
+
// Display summary
|
|
197
|
+
displaySummary(devboxId, categorized, isDryRun);
|
|
198
|
+
// Handle dry-run mode
|
|
199
|
+
if (isDryRun) {
|
|
200
|
+
console.log("\n(Dry run - no changes made)");
|
|
201
|
+
const result = {
|
|
202
|
+
sourceDevboxId: devboxId,
|
|
203
|
+
totalFound: snapshots.length,
|
|
204
|
+
successfulSnapshots: categorized.successful.length,
|
|
205
|
+
failedSnapshots: categorized.failed.length,
|
|
206
|
+
kept: categorized.toKeep,
|
|
207
|
+
deleted: [],
|
|
208
|
+
failed: [],
|
|
209
|
+
dryRun: true,
|
|
210
|
+
};
|
|
211
|
+
if (options.output && options.output !== "text") {
|
|
212
|
+
output(result, { format: options.output, defaultFormat: "json" });
|
|
213
|
+
}
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
// Handle nothing to delete
|
|
217
|
+
if (categorized.toDelete.length === 0) {
|
|
218
|
+
console.log("\nNothing to delete.");
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
// Warn if no successful snapshots
|
|
222
|
+
if (categorized.successful.length === 0) {
|
|
223
|
+
console.log("\nWarning: No ready snapshots found. Only deleting failed/incomplete snapshots.");
|
|
224
|
+
}
|
|
225
|
+
// Get confirmation unless --yes flag is set
|
|
226
|
+
if (!autoConfirm) {
|
|
227
|
+
const confirmed = await confirmDeletion(categorized.toDelete.length);
|
|
228
|
+
if (!confirmed) {
|
|
229
|
+
console.log("\nOperation cancelled.");
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// Perform deletions
|
|
234
|
+
console.log(`\nDeleting ${categorized.toDelete.length} snapshot${categorized.toDelete.length !== 1 ? "s" : ""}...`);
|
|
235
|
+
const deletionResults = await deleteSnapshotsWithTracking(categorized.toDelete);
|
|
236
|
+
// Display results
|
|
237
|
+
console.log("\nResults:");
|
|
238
|
+
console.log(` ✓ Successfully deleted: ${deletionResults.deleted.length} snapshot${deletionResults.deleted.length !== 1 ? "s" : ""}`);
|
|
239
|
+
// Show all deleted snapshots
|
|
240
|
+
displayDeletedSnapshots(deletionResults.deleted);
|
|
241
|
+
if (deletionResults.failed.length > 0) {
|
|
242
|
+
console.log(`\n ✗ Failed to delete: ${deletionResults.failed.length} snapshot${deletionResults.failed.length !== 1 ? "s" : ""}`);
|
|
243
|
+
for (const failure of deletionResults.failed) {
|
|
244
|
+
console.log(` - ${failure.id}: ${failure.error}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// Output structured data if requested
|
|
248
|
+
if (options.output && options.output !== "text") {
|
|
249
|
+
const result = {
|
|
250
|
+
sourceDevboxId: devboxId,
|
|
251
|
+
totalFound: snapshots.length,
|
|
252
|
+
successfulSnapshots: categorized.successful.length,
|
|
253
|
+
failedSnapshots: categorized.failed.length,
|
|
254
|
+
kept: categorized.toKeep,
|
|
255
|
+
deleted: deletionResults.deleted,
|
|
256
|
+
failed: deletionResults.failed,
|
|
257
|
+
dryRun: false,
|
|
258
|
+
};
|
|
259
|
+
output(result, { format: options.output, defaultFormat: "json" });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
outputError("Failed to prune snapshots", error);
|
|
264
|
+
}
|
|
265
|
+
}
|