@runloop/rl-cli 1.1.0 → 1.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 (55) hide show
  1. package/README.md +29 -8
  2. package/dist/commands/blueprint/list.js +97 -28
  3. package/dist/commands/blueprint/prune.js +258 -0
  4. package/dist/commands/devbox/create.js +3 -0
  5. package/dist/commands/devbox/list.js +44 -65
  6. package/dist/commands/menu.js +2 -1
  7. package/dist/commands/network-policy/create.js +27 -0
  8. package/dist/commands/network-policy/delete.js +21 -0
  9. package/dist/commands/network-policy/get.js +15 -0
  10. package/dist/commands/network-policy/list.js +494 -0
  11. package/dist/commands/object/list.js +516 -24
  12. package/dist/commands/snapshot/list.js +90 -29
  13. package/dist/components/Banner.js +109 -8
  14. package/dist/components/ConfirmationPrompt.js +45 -0
  15. package/dist/components/DevboxActionsMenu.js +42 -6
  16. package/dist/components/DevboxCard.js +1 -1
  17. package/dist/components/DevboxCreatePage.js +95 -81
  18. package/dist/components/DevboxDetailPage.js +218 -272
  19. package/dist/components/LogsViewer.js +8 -1
  20. package/dist/components/MainMenu.js +35 -4
  21. package/dist/components/NavigationTips.js +24 -0
  22. package/dist/components/NetworkPolicyCreatePage.js +264 -0
  23. package/dist/components/OperationsMenu.js +9 -1
  24. package/dist/components/ResourceActionsMenu.js +5 -1
  25. package/dist/components/ResourceDetailPage.js +204 -0
  26. package/dist/components/ResourceListView.js +19 -2
  27. package/dist/components/StatusBadge.js +2 -2
  28. package/dist/components/Table.js +6 -8
  29. package/dist/components/form/FormActionButton.js +7 -0
  30. package/dist/components/form/FormField.js +7 -0
  31. package/dist/components/form/FormListManager.js +112 -0
  32. package/dist/components/form/FormSelect.js +34 -0
  33. package/dist/components/form/FormTextInput.js +8 -0
  34. package/dist/components/form/index.js +8 -0
  35. package/dist/hooks/useViewportHeight.js +38 -20
  36. package/dist/router/Router.js +23 -1
  37. package/dist/screens/BlueprintDetailScreen.js +337 -0
  38. package/dist/screens/MenuScreen.js +6 -0
  39. package/dist/screens/NetworkPolicyCreateScreen.js +7 -0
  40. package/dist/screens/NetworkPolicyDetailScreen.js +247 -0
  41. package/dist/screens/NetworkPolicyListScreen.js +7 -0
  42. package/dist/screens/ObjectDetailScreen.js +377 -0
  43. package/dist/screens/ObjectListScreen.js +7 -0
  44. package/dist/screens/SnapshotDetailScreen.js +208 -0
  45. package/dist/services/blueprintService.js +30 -11
  46. package/dist/services/networkPolicyService.js +108 -0
  47. package/dist/services/objectService.js +101 -0
  48. package/dist/services/snapshotService.js +39 -3
  49. package/dist/store/blueprintStore.js +4 -10
  50. package/dist/store/index.js +1 -0
  51. package/dist/store/networkPolicyStore.js +83 -0
  52. package/dist/store/objectStore.js +92 -0
  53. package/dist/store/snapshotStore.js +4 -8
  54. package/dist/utils/commands.js +58 -0
  55. package/package.json +2 -2
@@ -0,0 +1,377 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * ObjectDetailScreen - Detail page for storage objects
4
+ * Uses the generic ResourceDetailPage component
5
+ */
6
+ import React from "react";
7
+ import { Box, Text, useInput } from "ink";
8
+ import TextInput from "ink-text-input";
9
+ import figures from "figures";
10
+ import { writeFile } from "fs/promises";
11
+ import { useNavigation } from "../store/navigationStore.js";
12
+ import { useObjectStore, } from "../store/objectStore.js";
13
+ import { getClient } from "../utils/client.js";
14
+ import { ResourceDetailPage, formatTimestamp, } from "../components/ResourceDetailPage.js";
15
+ import { getObject, deleteObject, formatFileSize, } from "../services/objectService.js";
16
+ import { SpinnerComponent } from "../components/Spinner.js";
17
+ import { ErrorMessage } from "../components/ErrorMessage.js";
18
+ import { SuccessMessage } from "../components/SuccessMessage.js";
19
+ import { Breadcrumb } from "../components/Breadcrumb.js";
20
+ import { Header } from "../components/Header.js";
21
+ import { ConfirmationPrompt } from "../components/ConfirmationPrompt.js";
22
+ import { colors } from "../utils/theme.js";
23
+ export function ObjectDetailScreen({ objectId }) {
24
+ const { goBack } = useNavigation();
25
+ const objects = useObjectStore((state) => state.objects);
26
+ const [loading, setLoading] = React.useState(false);
27
+ const [error, setError] = React.useState(null);
28
+ const [fetchedObject, setFetchedObject] = React.useState(null);
29
+ const [deleting, setDeleting] = React.useState(false);
30
+ const [showDownloadPrompt, setShowDownloadPrompt] = React.useState(false);
31
+ const [downloadPath, setDownloadPath] = React.useState("");
32
+ const [downloading, setDownloading] = React.useState(false);
33
+ const [downloadResult, setDownloadResult] = React.useState(null);
34
+ const [downloadError, setDownloadError] = React.useState(null);
35
+ const [showDeleteConfirm, setShowDeleteConfirm] = React.useState(false);
36
+ // Find object in store first
37
+ const objectFromStore = objects.find((o) => o.id === objectId);
38
+ // Polling function - must be defined before any early returns (Rules of Hooks)
39
+ const pollObject = React.useCallback(async () => {
40
+ if (!objectId)
41
+ return null;
42
+ return getObject(objectId);
43
+ }, [objectId]);
44
+ // Fetch object from API if not in store or missing full details
45
+ React.useEffect(() => {
46
+ if (objectId && !loading && !fetchedObject) {
47
+ // Always fetch full details since store may only have basic info
48
+ setLoading(true);
49
+ setError(null);
50
+ getObject(objectId)
51
+ .then((obj) => {
52
+ setFetchedObject(obj);
53
+ setLoading(false);
54
+ })
55
+ .catch((err) => {
56
+ setError(err);
57
+ setLoading(false);
58
+ });
59
+ }
60
+ }, [objectId, loading, fetchedObject]);
61
+ // Use fetched object for full details, fall back to store for basic display
62
+ const storageObject = fetchedObject || objectFromStore;
63
+ // Handle download submission
64
+ const handleDownloadSubmit = React.useCallback(async () => {
65
+ if (!downloadPath.trim() || !storageObject)
66
+ return;
67
+ setShowDownloadPrompt(false);
68
+ setDownloading(true);
69
+ try {
70
+ const client = getClient();
71
+ // Get download URL
72
+ const downloadUrlResponse = await client.objects.download(storageObject.id, {
73
+ duration_seconds: 3600,
74
+ });
75
+ // Download the file
76
+ const response = await fetch(downloadUrlResponse.download_url);
77
+ if (!response.ok) {
78
+ throw new Error(`Download failed: HTTP ${response.status}`);
79
+ }
80
+ // Save the file
81
+ const arrayBuffer = await response.arrayBuffer();
82
+ const buffer = Buffer.from(arrayBuffer);
83
+ await writeFile(downloadPath.trim(), buffer);
84
+ setDownloadResult(`Downloaded to ${downloadPath.trim()}`);
85
+ }
86
+ catch (err) {
87
+ setDownloadError(err);
88
+ }
89
+ finally {
90
+ setDownloading(false);
91
+ }
92
+ }, [downloadPath, storageObject]);
93
+ // Handle input for download prompt and result screens - must be before early returns (Rules of Hooks)
94
+ useInput((input, key) => {
95
+ if (showDownloadPrompt) {
96
+ if (key.escape) {
97
+ setShowDownloadPrompt(false);
98
+ setDownloadPath("");
99
+ }
100
+ else if (key.return) {
101
+ handleDownloadSubmit();
102
+ }
103
+ return;
104
+ }
105
+ if (downloadResult || downloadError) {
106
+ if (input === "q" || key.escape || key.return) {
107
+ setDownloadResult(null);
108
+ setDownloadError(null);
109
+ setDownloadPath("");
110
+ }
111
+ return;
112
+ }
113
+ }, { isActive: showDownloadPrompt || !!downloadResult || !!downloadError });
114
+ // Show loading state while fetching
115
+ if (loading && !storageObject) {
116
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
117
+ { label: "Storage Objects" },
118
+ { label: "Loading...", active: true },
119
+ ] }), _jsx(SpinnerComponent, { message: "Loading object details..." })] }));
120
+ }
121
+ // Show error state if fetch failed
122
+ if (error && !storageObject) {
123
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
124
+ { label: "Storage Objects" },
125
+ { label: "Error", active: true },
126
+ ] }), _jsx(ErrorMessage, { message: "Failed to load object details", error: error })] }));
127
+ }
128
+ // Show error if no object found
129
+ if (!storageObject) {
130
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
131
+ { label: "Storage Objects" },
132
+ { label: "Not Found", active: true },
133
+ ] }), _jsx(ErrorMessage, { message: `Storage object ${objectId || "unknown"} not found`, error: new Error("Storage object not found") })] }));
134
+ }
135
+ // Build detail sections
136
+ const detailSections = [];
137
+ // Basic details section
138
+ const basicFields = [];
139
+ if (storageObject.content_type) {
140
+ basicFields.push({
141
+ label: "Content Type",
142
+ value: storageObject.content_type,
143
+ });
144
+ }
145
+ if (storageObject.size_bytes !== undefined) {
146
+ basicFields.push({
147
+ label: "Size",
148
+ value: formatFileSize(storageObject.size_bytes),
149
+ });
150
+ }
151
+ if (storageObject.state) {
152
+ basicFields.push({
153
+ label: "State",
154
+ value: storageObject.state,
155
+ });
156
+ }
157
+ if (storageObject.is_public !== undefined) {
158
+ basicFields.push({
159
+ label: "Public",
160
+ value: storageObject.is_public ? "Yes" : "No",
161
+ });
162
+ }
163
+ if (storageObject.create_time_ms) {
164
+ basicFields.push({
165
+ label: "Created",
166
+ value: formatTimestamp(storageObject.create_time_ms),
167
+ });
168
+ }
169
+ // TTL / Expires - show remaining time before auto-deletion
170
+ if (storageObject.delete_after_time_ms) {
171
+ const now = Date.now();
172
+ const remainingMs = storageObject.delete_after_time_ms - now;
173
+ let ttlValue;
174
+ let ttlColor = colors.text;
175
+ if (remainingMs <= 0) {
176
+ ttlValue = "Expired";
177
+ ttlColor = colors.error;
178
+ }
179
+ else {
180
+ const remainingMinutes = Math.floor(remainingMs / 60000);
181
+ if (remainingMinutes < 60) {
182
+ ttlValue = `${remainingMinutes}m remaining`;
183
+ ttlColor = remainingMinutes < 10 ? colors.warning : colors.text;
184
+ }
185
+ else {
186
+ const hours = Math.floor(remainingMinutes / 60);
187
+ const mins = remainingMinutes % 60;
188
+ ttlValue = `${hours}h ${mins}m remaining`;
189
+ }
190
+ }
191
+ basicFields.push({
192
+ label: "Expires",
193
+ value: _jsx(Text, { color: ttlColor, children: ttlValue }),
194
+ });
195
+ }
196
+ if (basicFields.length > 0) {
197
+ detailSections.push({
198
+ title: "Details",
199
+ icon: figures.squareSmallFilled,
200
+ color: colors.warning,
201
+ fields: basicFields,
202
+ });
203
+ }
204
+ // Download URL section
205
+ if (storageObject.download_url) {
206
+ detailSections.push({
207
+ title: "Download",
208
+ icon: figures.arrowDown,
209
+ color: colors.success,
210
+ fields: [
211
+ {
212
+ label: "URL",
213
+ value: (_jsxs(Text, { color: colors.info, children: [storageObject.download_url.substring(0, 60), "..."] })),
214
+ },
215
+ ],
216
+ });
217
+ }
218
+ // Metadata section
219
+ if (storageObject.metadata &&
220
+ Object.keys(storageObject.metadata).length > 0) {
221
+ const metadataFields = Object.entries(storageObject.metadata).map(([key, value]) => ({
222
+ label: key,
223
+ value: value,
224
+ }));
225
+ detailSections.push({
226
+ title: "Metadata",
227
+ icon: figures.identical,
228
+ color: colors.secondary,
229
+ fields: metadataFields,
230
+ });
231
+ }
232
+ // Operations available for objects
233
+ const operations = [
234
+ {
235
+ key: "download",
236
+ label: "Download",
237
+ color: colors.success,
238
+ icon: figures.arrowDown,
239
+ shortcut: "w",
240
+ },
241
+ {
242
+ key: "delete",
243
+ label: "Delete",
244
+ color: colors.error,
245
+ icon: figures.cross,
246
+ shortcut: "d",
247
+ },
248
+ ];
249
+ // Handle operation selection
250
+ const handleOperation = async (operation, resource) => {
251
+ switch (operation) {
252
+ case "download":
253
+ // Show download prompt
254
+ const defaultName = resource.name || resource.id;
255
+ setDownloadPath(`./${defaultName}`);
256
+ setShowDownloadPrompt(true);
257
+ break;
258
+ case "delete":
259
+ // Show confirmation dialog
260
+ setShowDeleteConfirm(true);
261
+ break;
262
+ }
263
+ };
264
+ // Execute delete after confirmation
265
+ const executeDelete = async () => {
266
+ if (!storageObject)
267
+ return;
268
+ setShowDeleteConfirm(false);
269
+ setDeleting(true);
270
+ try {
271
+ await deleteObject(storageObject.id);
272
+ goBack();
273
+ }
274
+ catch (err) {
275
+ setError(err);
276
+ setDeleting(false);
277
+ }
278
+ };
279
+ // Build detailed info lines for full details view
280
+ const buildDetailLines = (obj) => {
281
+ const lines = [];
282
+ // Core Information
283
+ lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Storage Object Details" }, "core-title"));
284
+ lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", "ID: ", obj.id] }, "core-id"));
285
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Name: ", obj.name || "(none)"] }, "core-name"));
286
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Content Type: ", obj.content_type || "(unknown)"] }, "core-type"));
287
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Size: ", formatFileSize(obj.size_bytes)] }, "core-size"));
288
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "State: ", obj.state || "(unknown)"] }, "core-state"));
289
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Public: ", obj.is_public ? "Yes" : "No"] }, "core-public"));
290
+ if (obj.create_time_ms) {
291
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Created: ", new Date(obj.create_time_ms).toLocaleString()] }, "core-created"));
292
+ }
293
+ if (obj.delete_after_time_ms) {
294
+ const now = Date.now();
295
+ const remainingMs = obj.delete_after_time_ms - now;
296
+ let expiresText;
297
+ if (remainingMs <= 0) {
298
+ expiresText = "Expired";
299
+ }
300
+ else {
301
+ const remainingMinutes = Math.floor(remainingMs / 60000);
302
+ if (remainingMinutes < 60) {
303
+ expiresText = `${remainingMinutes}m remaining`;
304
+ }
305
+ else {
306
+ const hours = Math.floor(remainingMinutes / 60);
307
+ const mins = remainingMinutes % 60;
308
+ expiresText = `${hours}h ${mins}m remaining`;
309
+ }
310
+ }
311
+ lines.push(_jsxs(Text, { color: remainingMs <= 0 ? colors.error : colors.warning, children: [" ", "Expires: ", expiresText] }, "core-expires"));
312
+ }
313
+ lines.push(_jsx(Text, { children: " " }, "core-space"));
314
+ // Download URL
315
+ if (obj.download_url) {
316
+ lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Download URL" }, "url-title"));
317
+ lines.push(_jsxs(Text, { color: colors.info, children: [" ", obj.download_url] }, "url-value"));
318
+ lines.push(_jsx(Text, { children: " " }, "url-space"));
319
+ }
320
+ // Metadata
321
+ if (obj.metadata && Object.keys(obj.metadata).length > 0) {
322
+ lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Metadata" }, "meta-title"));
323
+ Object.entries(obj.metadata).forEach(([key, value], idx) => {
324
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", key, ": ", value] }, `meta-${idx}`));
325
+ });
326
+ lines.push(_jsx(Text, { children: " " }, "meta-space"));
327
+ }
328
+ // Raw JSON
329
+ lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Raw JSON" }, "json-title"));
330
+ const jsonLines = JSON.stringify(obj, null, 2).split("\n");
331
+ jsonLines.forEach((line, idx) => {
332
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", line] }, `json-${idx}`));
333
+ });
334
+ return lines;
335
+ };
336
+ // Show download result
337
+ if (downloadResult || downloadError) {
338
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
339
+ { label: "Storage Objects" },
340
+ { label: storageObject.name || storageObject.id },
341
+ { label: "Download", active: true },
342
+ ] }), _jsx(Header, { title: "Download Result" }), downloadResult && _jsx(SuccessMessage, { message: downloadResult }), downloadError && (_jsx(ErrorMessage, { message: "Download failed", error: downloadError })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: "Press [Enter], [q], or [esc] to continue" }) })] }));
343
+ }
344
+ // Show downloading state
345
+ if (downloading) {
346
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
347
+ { label: "Storage Objects" },
348
+ { label: storageObject.name || storageObject.id },
349
+ { label: "Downloading...", active: true },
350
+ ] }), _jsx(SpinnerComponent, { message: `Downloading to ${downloadPath}...` })] }));
351
+ }
352
+ // Show download prompt
353
+ if (showDownloadPrompt) {
354
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
355
+ { label: "Storage Objects" },
356
+ { label: storageObject.name || storageObject.id },
357
+ { label: "Download", active: true },
358
+ ] }), _jsx(Header, { title: "Download Storage Object" }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: colors.text, children: [figures.arrowRight, " Downloading:", " ", _jsx(Text, { color: colors.primary, children: storageObject.name || storageObject.id })] }), storageObject.size_bytes && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.info, " Size: ", formatFileSize(storageObject.size_bytes)] }))] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.text, children: "Save to path:" }), _jsxs(Box, { marginTop: 0, children: [_jsxs(Text, { color: colors.primary, children: [figures.pointer, " "] }), _jsx(TextInput, { value: downloadPath, onChange: setDownloadPath, placeholder: "./filename" })] })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: "[Enter] Download \u2022 [Esc] Cancel" }) })] }));
359
+ }
360
+ // Show delete confirmation
361
+ if (showDeleteConfirm && storageObject) {
362
+ return (_jsx(ConfirmationPrompt, { title: "Delete Storage Object", message: `Are you sure you want to delete "${storageObject.name || storageObject.id}"?`, details: "This action cannot be undone.", breadcrumbItems: [
363
+ { label: "Storage Objects" },
364
+ { label: storageObject.name || storageObject.id },
365
+ { label: "Delete", active: true },
366
+ ], onConfirm: executeDelete, onCancel: () => setShowDeleteConfirm(false) }));
367
+ }
368
+ // Show deleting state
369
+ if (deleting) {
370
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
371
+ { label: "Storage Objects" },
372
+ { label: storageObject.name || storageObject.id },
373
+ { label: "Deleting...", active: true },
374
+ ] }), _jsx(SpinnerComponent, { message: "Deleting storage object..." })] }));
375
+ }
376
+ return (_jsx(ResourceDetailPage, { resource: storageObject, resourceType: "Storage Objects", getDisplayName: (obj) => obj.name || obj.id, getId: (obj) => obj.id, getStatus: (obj) => obj.state || "unknown", detailSections: detailSections, operations: operations, onOperation: handleOperation, onBack: goBack, buildDetailLines: buildDetailLines, pollResource: pollObject }));
377
+ }
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useNavigation } from "../store/navigationStore.js";
3
+ import { ListObjectsUI } from "../commands/object/list.js";
4
+ export function ObjectListScreen() {
5
+ const { goBack } = useNavigation();
6
+ return _jsx(ListObjectsUI, { onBack: goBack, onExit: goBack });
7
+ }
@@ -0,0 +1,208 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * SnapshotDetailScreen - Detail page for snapshots
4
+ * Uses the generic ResourceDetailPage component
5
+ */
6
+ import React from "react";
7
+ import { Text } from "ink";
8
+ import figures from "figures";
9
+ import { useNavigation } from "../store/navigationStore.js";
10
+ import { useSnapshotStore } from "../store/snapshotStore.js";
11
+ import { ResourceDetailPage, formatTimestamp, } from "../components/ResourceDetailPage.js";
12
+ import { getSnapshot, deleteSnapshot } from "../services/snapshotService.js";
13
+ import { SpinnerComponent } from "../components/Spinner.js";
14
+ import { ErrorMessage } from "../components/ErrorMessage.js";
15
+ import { Breadcrumb } from "../components/Breadcrumb.js";
16
+ import { ConfirmationPrompt } from "../components/ConfirmationPrompt.js";
17
+ import { colors } from "../utils/theme.js";
18
+ export function SnapshotDetailScreen({ snapshotId, }) {
19
+ const { goBack, navigate } = useNavigation();
20
+ const snapshots = useSnapshotStore((state) => state.snapshots);
21
+ const [loading, setLoading] = React.useState(false);
22
+ const [error, setError] = React.useState(null);
23
+ const [fetchedSnapshot, setFetchedSnapshot] = React.useState(null);
24
+ const [deleting, setDeleting] = React.useState(false);
25
+ const [showDeleteConfirm, setShowDeleteConfirm] = React.useState(false);
26
+ // Find snapshot in store first
27
+ const snapshotFromStore = snapshots.find((s) => s.id === snapshotId);
28
+ // Polling function - must be defined before any early returns (Rules of Hooks)
29
+ const pollSnapshot = React.useCallback(async () => {
30
+ if (!snapshotId)
31
+ return null;
32
+ return getSnapshot(snapshotId);
33
+ }, [snapshotId]);
34
+ // Fetch snapshot from API if not in store or missing full details
35
+ React.useEffect(() => {
36
+ if (snapshotId && !loading && !fetchedSnapshot) {
37
+ // Always fetch full details since store may only have basic info
38
+ setLoading(true);
39
+ setError(null);
40
+ getSnapshot(snapshotId)
41
+ .then((snapshot) => {
42
+ setFetchedSnapshot(snapshot);
43
+ setLoading(false);
44
+ })
45
+ .catch((err) => {
46
+ setError(err);
47
+ setLoading(false);
48
+ });
49
+ }
50
+ }, [snapshotId, loading, fetchedSnapshot]);
51
+ // Use fetched snapshot for full details, fall back to store for basic display
52
+ const snapshot = fetchedSnapshot || snapshotFromStore;
53
+ // Show loading state while fetching
54
+ if (loading && !snapshot) {
55
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
56
+ { label: "Snapshots" },
57
+ { label: "Loading...", active: true },
58
+ ] }), _jsx(SpinnerComponent, { message: "Loading snapshot details..." })] }));
59
+ }
60
+ // Show error state if fetch failed
61
+ if (error && !snapshot) {
62
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Snapshots" }, { label: "Error", active: true }] }), _jsx(ErrorMessage, { message: "Failed to load snapshot details", error: error })] }));
63
+ }
64
+ // Show error if no snapshot found
65
+ if (!snapshot) {
66
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Snapshots" }, { label: "Not Found", active: true }] }), _jsx(ErrorMessage, { message: `Snapshot ${snapshotId || "unknown"} not found`, error: new Error("Snapshot not found") })] }));
67
+ }
68
+ // Build detail sections
69
+ const detailSections = [];
70
+ // Basic details section
71
+ const basicFields = [];
72
+ if (snapshot.create_time_ms) {
73
+ basicFields.push({
74
+ label: "Created",
75
+ value: formatTimestamp(snapshot.create_time_ms),
76
+ });
77
+ }
78
+ if (snapshot.devbox_id) {
79
+ basicFields.push({
80
+ label: "Source Devbox",
81
+ value: _jsx(Text, { color: colors.idColor, children: snapshot.devbox_id }),
82
+ });
83
+ }
84
+ if (snapshot.disk_size_bytes) {
85
+ const sizeGB = (snapshot.disk_size_bytes / (1024 * 1024 * 1024)).toFixed(2);
86
+ basicFields.push({
87
+ label: "Disk Size",
88
+ value: `${sizeGB} GB`,
89
+ });
90
+ }
91
+ if (basicFields.length > 0) {
92
+ detailSections.push({
93
+ title: "Details",
94
+ icon: figures.squareSmallFilled,
95
+ color: colors.warning,
96
+ fields: basicFields,
97
+ });
98
+ }
99
+ // Metadata section
100
+ if (snapshot.metadata && Object.keys(snapshot.metadata).length > 0) {
101
+ const metadataFields = Object.entries(snapshot.metadata).map(([key, value]) => ({
102
+ label: key,
103
+ value: value,
104
+ }));
105
+ detailSections.push({
106
+ title: "Metadata",
107
+ icon: figures.identical,
108
+ color: colors.secondary,
109
+ fields: metadataFields,
110
+ });
111
+ }
112
+ // Operations available for snapshots
113
+ const operations = [
114
+ {
115
+ key: "create-devbox",
116
+ label: "Create Devbox from Snapshot",
117
+ color: colors.success,
118
+ icon: figures.play,
119
+ shortcut: "c",
120
+ },
121
+ {
122
+ key: "delete",
123
+ label: "Delete Snapshot",
124
+ color: colors.error,
125
+ icon: figures.cross,
126
+ shortcut: "d",
127
+ },
128
+ ];
129
+ // Handle operation selection
130
+ const handleOperation = async (operation, resource) => {
131
+ switch (operation) {
132
+ case "create-devbox":
133
+ navigate("devbox-create", { snapshotId: resource.id });
134
+ break;
135
+ case "delete":
136
+ // Show confirmation dialog
137
+ setShowDeleteConfirm(true);
138
+ break;
139
+ }
140
+ };
141
+ // Execute delete after confirmation
142
+ const executeDelete = async () => {
143
+ if (!snapshot)
144
+ return;
145
+ setShowDeleteConfirm(false);
146
+ setDeleting(true);
147
+ try {
148
+ await deleteSnapshot(snapshot.id);
149
+ goBack();
150
+ }
151
+ catch (err) {
152
+ setError(err);
153
+ setDeleting(false);
154
+ }
155
+ };
156
+ // Build detailed info lines for full details view
157
+ const buildDetailLines = (snap) => {
158
+ const lines = [];
159
+ // Core Information
160
+ lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Snapshot Details" }, "core-title"));
161
+ lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", "ID: ", snap.id] }, "core-id"));
162
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Name: ", snap.name || "(none)"] }, "core-name"));
163
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Status: ", snap.status] }, "core-status"));
164
+ if (snap.devbox_id) {
165
+ lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", "Source Devbox: ", snap.devbox_id] }, "core-devbox"));
166
+ }
167
+ if (snap.create_time_ms) {
168
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Created: ", new Date(snap.create_time_ms).toLocaleString()] }, "core-created"));
169
+ }
170
+ if (snap.disk_size_bytes) {
171
+ const sizeGB = (snap.disk_size_bytes / (1024 * 1024 * 1024)).toFixed(2);
172
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Disk Size: ", sizeGB, " GB"] }, "core-size"));
173
+ }
174
+ lines.push(_jsx(Text, { children: " " }, "core-space"));
175
+ // Metadata
176
+ if (snap.metadata && Object.keys(snap.metadata).length > 0) {
177
+ lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Metadata" }, "meta-title"));
178
+ Object.entries(snap.metadata).forEach(([key, value], idx) => {
179
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", key, ": ", value] }, `meta-${idx}`));
180
+ });
181
+ lines.push(_jsx(Text, { children: " " }, "meta-space"));
182
+ }
183
+ // Raw JSON
184
+ lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Raw JSON" }, "json-title"));
185
+ const jsonLines = JSON.stringify(snap, null, 2).split("\n");
186
+ jsonLines.forEach((line, idx) => {
187
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", line] }, `json-${idx}`));
188
+ });
189
+ return lines;
190
+ };
191
+ // Show delete confirmation
192
+ if (showDeleteConfirm && snapshot) {
193
+ return (_jsx(ConfirmationPrompt, { title: "Delete Snapshot", message: `Are you sure you want to delete "${snapshot.name || snapshot.id}"?`, details: "This action cannot be undone.", breadcrumbItems: [
194
+ { label: "Snapshots" },
195
+ { label: snapshot.name || snapshot.id },
196
+ { label: "Delete", active: true },
197
+ ], onConfirm: executeDelete, onCancel: () => setShowDeleteConfirm(false) }));
198
+ }
199
+ // Show deleting state
200
+ if (deleting) {
201
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
202
+ { label: "Snapshots" },
203
+ { label: snapshot.name || snapshot.id },
204
+ { label: "Deleting...", active: true },
205
+ ] }), _jsx(SpinnerComponent, { message: "Deleting snapshot..." })] }));
206
+ }
207
+ return (_jsx(ResourceDetailPage, { resource: snapshot, resourceType: "Snapshots", getDisplayName: (snap) => snap.name || snap.id, getId: (snap) => snap.id, getStatus: (snap) => snap.status || "unknown", detailSections: detailSections, operations: operations, onOperation: handleOperation, onBack: goBack, buildDetailLines: buildDetailLines, pollResource: snapshot.status === "pending" ? pollSnapshot : undefined }));
208
+ }
@@ -33,11 +33,11 @@ export async function listBlueprints(options) {
33
33
  blueprints.push({
34
34
  id: String(b.id || "").substring(0, MAX_ID_LENGTH),
35
35
  name: String(b.name || "").substring(0, MAX_NAME_LENGTH),
36
- status: String(b.status || "").substring(0, MAX_STATUS_LENGTH),
36
+ status: b.status,
37
+ state: b.state,
37
38
  create_time_ms: b.create_time_ms,
38
- build_status: b.status
39
- ? String(b.status).substring(0, MAX_STATUS_LENGTH)
40
- : undefined,
39
+ parameters: b.parameters,
40
+ // UI-specific convenience fields
41
41
  architecture: architecture
42
42
  ? String(architecture).substring(0, MAX_ARCH_LENGTH)
43
43
  : undefined,
@@ -60,16 +60,35 @@ export async function listBlueprints(options) {
60
60
  export async function getBlueprint(id) {
61
61
  const client = getClient();
62
62
  const blueprint = await client.blueprints.retrieve(id);
63
+ // Extract architecture and resources from launch_parameters for convenience
64
+ const launchParams = blueprint.parameters?.launch_parameters;
63
65
  return {
64
- id: blueprint.id,
65
- name: blueprint.name,
66
- status: blueprint.status,
67
- create_time_ms: blueprint.create_time_ms,
68
- build_status: blueprint.build_status,
69
- architecture: blueprint.architecture,
70
- resources: blueprint.resources,
66
+ // Spread all API fields
67
+ ...blueprint,
68
+ // UI-specific convenience fields
69
+ architecture: launchParams?.architecture ?? undefined,
70
+ resources: launchParams?.resource_size_request ?? undefined,
71
71
  };
72
72
  }
73
+ /**
74
+ * Get a single blueprint by ID or name
75
+ */
76
+ export async function getBlueprintByIdOrName(idOrName) {
77
+ const client = getClient();
78
+ // Check if it's an ID (starts with bpt_) or a name
79
+ if (idOrName.startsWith("bpt_")) {
80
+ return getBlueprint(idOrName);
81
+ }
82
+ // It's a name, search for it
83
+ const result = await client.blueprints.list({ name: idOrName });
84
+ const blueprints = result.blueprints || [];
85
+ if (blueprints.length === 0) {
86
+ return null;
87
+ }
88
+ // Return the first exact match, or first result if no exact match
89
+ const blueprint = blueprints.find((b) => b.name === idOrName) || blueprints[0];
90
+ return getBlueprint(blueprint.id);
91
+ }
73
92
  /**
74
93
  * Get blueprint logs
75
94
  * Returns the raw logs array from the API response