@runloop/rl-cli 0.1.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -10
- package/dist/cli.js +79 -72
- package/dist/commands/auth.js +2 -2
- package/dist/commands/blueprint/create.js +31 -83
- package/dist/commands/blueprint/get.js +29 -34
- package/dist/commands/blueprint/list.js +278 -230
- package/dist/commands/blueprint/logs.js +133 -37
- package/dist/commands/config.js +118 -0
- package/dist/commands/devbox/create.js +120 -40
- package/dist/commands/devbox/delete.js +17 -33
- package/dist/commands/devbox/download.js +29 -43
- package/dist/commands/devbox/exec.js +22 -39
- package/dist/commands/devbox/execAsync.js +20 -37
- package/dist/commands/devbox/get.js +13 -35
- package/dist/commands/devbox/getAsync.js +12 -34
- package/dist/commands/devbox/list.js +241 -402
- package/dist/commands/devbox/logs.js +20 -38
- package/dist/commands/devbox/read.js +29 -43
- package/dist/commands/devbox/resume.js +13 -35
- package/dist/commands/devbox/rsync.js +26 -78
- package/dist/commands/devbox/scp.js +25 -79
- package/dist/commands/devbox/sendStdin.js +41 -0
- package/dist/commands/devbox/shutdown.js +13 -35
- package/dist/commands/devbox/ssh.js +46 -78
- package/dist/commands/devbox/suspend.js +13 -35
- package/dist/commands/devbox/tunnel.js +37 -88
- package/dist/commands/devbox/upload.js +28 -36
- package/dist/commands/devbox/write.js +29 -44
- package/dist/commands/mcp-http.js +6 -5
- package/dist/commands/mcp-install.js +12 -10
- package/dist/commands/mcp.js +5 -4
- package/dist/commands/menu.js +26 -67
- package/dist/commands/object/delete.js +12 -34
- package/dist/commands/object/download.js +26 -74
- package/dist/commands/object/get.js +12 -34
- package/dist/commands/object/list.js +15 -93
- package/dist/commands/object/upload.js +35 -96
- package/dist/commands/snapshot/create.js +23 -39
- package/dist/commands/snapshot/delete.js +17 -33
- package/dist/commands/snapshot/get.js +16 -0
- package/dist/commands/snapshot/list.js +309 -80
- package/dist/commands/snapshot/status.js +12 -34
- package/dist/components/ActionsPopup.js +64 -39
- package/dist/components/Banner.js +7 -1
- package/dist/components/Breadcrumb.js +11 -48
- package/dist/components/DevboxActionsMenu.js +117 -207
- package/dist/components/DevboxCreatePage.js +12 -7
- package/dist/components/DevboxDetailPage.js +76 -28
- package/dist/components/ErrorBoundary.js +29 -0
- package/dist/components/ErrorMessage.js +10 -2
- package/dist/components/Header.js +12 -4
- package/dist/components/InteractiveSpawn.js +104 -0
- package/dist/components/LogsViewer.js +169 -0
- package/dist/components/MainMenu.js +37 -33
- package/dist/components/MetadataDisplay.js +4 -4
- package/dist/components/OperationsMenu.js +1 -1
- package/dist/components/ResourceActionsMenu.js +4 -4
- package/dist/components/ResourceListView.js +46 -34
- package/dist/components/Spinner.js +7 -2
- package/dist/components/StatusBadge.js +1 -1
- package/dist/components/SuccessMessage.js +12 -2
- package/dist/components/Table.js +16 -6
- package/dist/components/UpdateNotification.js +56 -0
- package/dist/hooks/useCursorPagination.js +125 -85
- package/dist/hooks/useExitOnCtrlC.js +15 -0
- package/dist/hooks/useViewportHeight.js +47 -0
- package/dist/mcp/server-http.js +2 -1
- package/dist/mcp/server.js +71 -7
- package/dist/router/Router.js +70 -0
- package/dist/router/types.js +1 -0
- package/dist/screens/BlueprintListScreen.js +7 -0
- package/dist/screens/BlueprintLogsScreen.js +74 -0
- package/dist/screens/DevboxActionsScreen.js +25 -0
- package/dist/screens/DevboxCreateScreen.js +11 -0
- package/dist/screens/DevboxDetailScreen.js +60 -0
- package/dist/screens/DevboxListScreen.js +23 -0
- package/dist/screens/LogsSessionScreen.js +49 -0
- package/dist/screens/MenuScreen.js +23 -0
- package/dist/screens/SSHSessionScreen.js +55 -0
- package/dist/screens/SnapshotListScreen.js +7 -0
- package/dist/services/blueprintService.js +101 -0
- package/dist/services/devboxService.js +215 -0
- package/dist/services/snapshotService.js +81 -0
- package/dist/store/blueprintStore.js +89 -0
- package/dist/store/devboxStore.js +105 -0
- package/dist/store/index.js +7 -0
- package/dist/store/navigationStore.js +101 -0
- package/dist/store/snapshotStore.js +87 -0
- package/dist/utils/client.js +4 -2
- package/dist/utils/config.js +22 -111
- package/dist/utils/interactiveCommand.js +3 -2
- package/dist/utils/logFormatter.js +208 -0
- package/dist/utils/memoryMonitor.js +85 -0
- package/dist/utils/output.js +153 -61
- package/dist/utils/process.js +106 -0
- package/dist/utils/processUtils.js +135 -0
- package/dist/utils/screen.js +61 -0
- package/dist/utils/ssh.js +6 -3
- package/dist/utils/sshSession.js +5 -29
- package/dist/utils/terminalDetection.js +185 -0
- package/dist/utils/terminalSync.js +39 -0
- package/dist/utils/theme.js +162 -13
- package/dist/utils/versionCheck.js +53 -0
- package/dist/version.js +12 -0
- package/package.json +19 -17
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import React from "react";
|
|
3
|
-
import { Box, Text, useInput,
|
|
3
|
+
import { Box, Text, useInput, useApp } from "ink";
|
|
4
4
|
import TextInput from "ink-text-input";
|
|
5
5
|
import figures from "figures";
|
|
6
6
|
import { getClient } from "../../utils/client.js";
|
|
@@ -12,43 +12,157 @@ import { Breadcrumb } from "../../components/Breadcrumb.js";
|
|
|
12
12
|
import { createTextColumn, Table } from "../../components/Table.js";
|
|
13
13
|
import { ActionsPopup } from "../../components/ActionsPopup.js";
|
|
14
14
|
import { formatTimeAgo } from "../../components/ResourceListView.js";
|
|
15
|
-
import {
|
|
15
|
+
import { output, outputError } from "../../utils/output.js";
|
|
16
16
|
import { getBlueprintUrl } from "../../utils/url.js";
|
|
17
17
|
import { colors } from "../../utils/theme.js";
|
|
18
18
|
import { getStatusDisplay } from "../../components/StatusBadge.js";
|
|
19
19
|
import { DevboxCreatePage } from "../../components/DevboxCreatePage.js";
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
import { useExitOnCtrlC } from "../../hooks/useExitOnCtrlC.js";
|
|
21
|
+
import { useViewportHeight } from "../../hooks/useViewportHeight.js";
|
|
22
|
+
import { useCursorPagination } from "../../hooks/useCursorPagination.js";
|
|
23
|
+
import { useNavigation } from "../../store/navigationStore.js";
|
|
24
|
+
const DEFAULT_PAGE_SIZE = 10;
|
|
25
|
+
const ListBlueprintsUI = ({ onBack, onExit, }) => {
|
|
26
|
+
const { exit: inkExit } = useApp();
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
24
28
|
const [selectedBlueprint, setSelectedBlueprint] = React.useState(null);
|
|
25
29
|
const [selectedOperation, setSelectedOperation] = React.useState(0);
|
|
26
30
|
const [executingOperation, setExecutingOperation] = React.useState(null);
|
|
27
31
|
const [operationInput, setOperationInput] = React.useState("");
|
|
28
32
|
const [operationResult, setOperationResult] = React.useState(null);
|
|
29
33
|
const [operationError, setOperationError] = React.useState(null);
|
|
30
|
-
const [
|
|
34
|
+
const [operationLoading, setOperationLoading] = React.useState(false);
|
|
31
35
|
const [showCreateDevbox, setShowCreateDevbox] = React.useState(false);
|
|
32
|
-
// List view state - moved to top to ensure hooks are called in same order
|
|
33
|
-
const [blueprints, setBlueprints] = React.useState([]);
|
|
34
|
-
const [listError, setListError] = React.useState(null);
|
|
35
|
-
const [currentPage, setCurrentPage] = React.useState(0);
|
|
36
36
|
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
|
37
|
-
const [showActions, setShowActions] = React.useState(false);
|
|
38
37
|
const [showPopup, setShowPopup] = React.useState(false);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
38
|
+
const { navigate } = useNavigation();
|
|
39
|
+
// Calculate overhead for viewport height
|
|
40
|
+
const overhead = 13;
|
|
41
|
+
const { viewportHeight, terminalWidth } = useViewportHeight({
|
|
42
|
+
overhead,
|
|
43
|
+
minHeight: 5,
|
|
44
|
+
});
|
|
45
|
+
const PAGE_SIZE = viewportHeight;
|
|
46
|
+
// All width constants
|
|
42
47
|
const statusIconWidth = 2;
|
|
43
48
|
const statusTextWidth = 10;
|
|
44
49
|
const idWidth = 25;
|
|
45
|
-
const nameWidth = terminalWidth >= 120 ? 30 : 25;
|
|
50
|
+
const nameWidth = Math.max(15, terminalWidth >= 120 ? 30 : 25);
|
|
46
51
|
const descriptionWidth = 40;
|
|
47
52
|
const timeWidth = 20;
|
|
53
|
+
const showDescription = terminalWidth >= 120;
|
|
54
|
+
// Fetch function for pagination hook
|
|
55
|
+
const fetchPage = React.useCallback(async (params) => {
|
|
56
|
+
const client = getClient();
|
|
57
|
+
const pageBlueprints = [];
|
|
58
|
+
// Build query params
|
|
59
|
+
const queryParams = {
|
|
60
|
+
limit: params.limit,
|
|
61
|
+
};
|
|
62
|
+
if (params.startingAt) {
|
|
63
|
+
queryParams.starting_after = params.startingAt;
|
|
64
|
+
}
|
|
65
|
+
// Fetch ONE page only
|
|
66
|
+
const page = (await client.blueprints.list(queryParams));
|
|
67
|
+
// Extract data and create defensive copies
|
|
68
|
+
if (page.blueprints && Array.isArray(page.blueprints)) {
|
|
69
|
+
page.blueprints.forEach((b) => {
|
|
70
|
+
pageBlueprints.push({
|
|
71
|
+
id: b.id,
|
|
72
|
+
name: b.name,
|
|
73
|
+
status: b.status,
|
|
74
|
+
create_time_ms: b.create_time_ms,
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
const result = {
|
|
79
|
+
items: pageBlueprints,
|
|
80
|
+
hasMore: page.has_more || false,
|
|
81
|
+
totalCount: page.total_count || pageBlueprints.length,
|
|
82
|
+
};
|
|
83
|
+
return result;
|
|
84
|
+
}, []);
|
|
85
|
+
// Use the shared pagination hook
|
|
86
|
+
const { items: blueprints, loading, navigating, error: listError, currentPage, hasMore, hasPrev, totalCount, nextPage, prevPage, } = useCursorPagination({
|
|
87
|
+
fetchPage,
|
|
88
|
+
pageSize: PAGE_SIZE,
|
|
89
|
+
getItemId: (blueprint) => blueprint.id,
|
|
90
|
+
pollInterval: 2000,
|
|
91
|
+
pollingEnabled: !showPopup && !showCreateDevbox && !executingOperation,
|
|
92
|
+
deps: [PAGE_SIZE],
|
|
93
|
+
});
|
|
94
|
+
// Memoize columns array
|
|
95
|
+
const blueprintColumns = React.useMemo(() => [
|
|
96
|
+
{
|
|
97
|
+
key: "statusIcon",
|
|
98
|
+
label: "",
|
|
99
|
+
width: statusIconWidth,
|
|
100
|
+
render: (blueprint, _index, isSelected) => {
|
|
101
|
+
const statusDisplay = getStatusDisplay(blueprint.status || "");
|
|
102
|
+
const statusColor = statusDisplay.color === colors.textDim
|
|
103
|
+
? colors.info
|
|
104
|
+
: statusDisplay.color;
|
|
105
|
+
return (_jsxs(Text, { color: isSelected ? "white" : statusColor, bold: true, dimColor: false, inverse: isSelected, wrap: "truncate", children: [statusDisplay.icon, " "] }));
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
key: "id",
|
|
110
|
+
label: "ID",
|
|
111
|
+
width: idWidth + 1,
|
|
112
|
+
render: (blueprint, _index, isSelected) => {
|
|
113
|
+
const value = blueprint.id;
|
|
114
|
+
const width = Math.max(1, idWidth + 1);
|
|
115
|
+
const truncated = value.slice(0, Math.max(1, width - 1));
|
|
116
|
+
const padded = truncated.padEnd(width, " ");
|
|
117
|
+
return (_jsx(Text, { color: isSelected ? "white" : colors.idColor, bold: false, dimColor: false, inverse: isSelected, wrap: "truncate", children: padded }));
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
key: "statusText",
|
|
122
|
+
label: "Status",
|
|
123
|
+
width: statusTextWidth,
|
|
124
|
+
render: (blueprint, _index, isSelected) => {
|
|
125
|
+
const statusDisplay = getStatusDisplay(blueprint.status || "");
|
|
126
|
+
const statusColor = statusDisplay.color === colors.textDim
|
|
127
|
+
? colors.info
|
|
128
|
+
: statusDisplay.color;
|
|
129
|
+
const safeWidth = Math.max(1, statusTextWidth);
|
|
130
|
+
const truncated = statusDisplay.text.slice(0, safeWidth);
|
|
131
|
+
const padded = truncated.padEnd(safeWidth, " ");
|
|
132
|
+
return (_jsx(Text, { color: isSelected ? "white" : statusColor, bold: true, dimColor: false, inverse: isSelected, wrap: "truncate", children: padded }));
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
createTextColumn("name", "Name", (blueprint) => blueprint.name || "", {
|
|
136
|
+
width: nameWidth,
|
|
137
|
+
}),
|
|
138
|
+
// Description column removed - not available in API
|
|
139
|
+
createTextColumn("created", "Created", (blueprint) => blueprint.create_time_ms
|
|
140
|
+
? formatTimeAgo(blueprint.create_time_ms)
|
|
141
|
+
: "", {
|
|
142
|
+
width: timeWidth,
|
|
143
|
+
color: colors.textDim,
|
|
144
|
+
dimColor: false,
|
|
145
|
+
bold: false,
|
|
146
|
+
}),
|
|
147
|
+
], [
|
|
148
|
+
statusIconWidth,
|
|
149
|
+
statusTextWidth,
|
|
150
|
+
idWidth,
|
|
151
|
+
nameWidth,
|
|
152
|
+
descriptionWidth,
|
|
153
|
+
timeWidth,
|
|
154
|
+
showDescription,
|
|
155
|
+
]);
|
|
48
156
|
// Helper function to generate operations based on selected blueprint
|
|
49
157
|
const getOperationsForBlueprint = (blueprint) => {
|
|
50
158
|
const operations = [];
|
|
51
|
-
//
|
|
159
|
+
// View Logs is always available
|
|
160
|
+
operations.push({
|
|
161
|
+
key: "view_logs",
|
|
162
|
+
label: "View Logs",
|
|
163
|
+
color: colors.info,
|
|
164
|
+
icon: figures.info,
|
|
165
|
+
});
|
|
52
166
|
if (blueprint &&
|
|
53
167
|
(blueprint.status === "build_complete" ||
|
|
54
168
|
blueprint.status === "building_complete")) {
|
|
@@ -59,7 +173,6 @@ const ListBlueprintsUI = ({ onBack, onExit }) => {
|
|
|
59
173
|
icon: figures.play,
|
|
60
174
|
});
|
|
61
175
|
}
|
|
62
|
-
// Always show delete option
|
|
63
176
|
operations.push({
|
|
64
177
|
key: "delete",
|
|
65
178
|
label: "Delete Blueprint",
|
|
@@ -68,57 +181,97 @@ const ListBlueprintsUI = ({ onBack, onExit }) => {
|
|
|
68
181
|
});
|
|
69
182
|
return operations;
|
|
70
183
|
};
|
|
71
|
-
//
|
|
184
|
+
// Handle Ctrl+C to exit
|
|
185
|
+
useExitOnCtrlC();
|
|
186
|
+
// Ensure selected index is within bounds
|
|
72
187
|
React.useEffect(() => {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
188
|
+
if (blueprints.length > 0 && selectedIndex >= blueprints.length) {
|
|
189
|
+
setSelectedIndex(Math.max(0, blueprints.length - 1));
|
|
190
|
+
}
|
|
191
|
+
}, [blueprints.length, selectedIndex]);
|
|
192
|
+
const selectedBlueprintItem = blueprints[selectedIndex];
|
|
193
|
+
const allOperations = getOperationsForBlueprint(selectedBlueprintItem);
|
|
194
|
+
// Calculate pagination info for display
|
|
195
|
+
const totalPages = Math.max(1, Math.ceil(totalCount / PAGE_SIZE));
|
|
196
|
+
const startIndex = currentPage * PAGE_SIZE;
|
|
197
|
+
const endIndex = startIndex + blueprints.length;
|
|
198
|
+
const executeOperation = async (blueprintOverride, operationOverride) => {
|
|
199
|
+
const client = getClient();
|
|
200
|
+
// Use override if provided, otherwise use selectedBlueprint from state
|
|
201
|
+
// If neither is available, use selectedBlueprintItem as fallback
|
|
202
|
+
const blueprint = blueprintOverride || selectedBlueprint || selectedBlueprintItem;
|
|
203
|
+
// Use operation override if provided (to avoid state timing issues)
|
|
204
|
+
const operation = operationOverride || executingOperation;
|
|
205
|
+
if (!blueprint) {
|
|
206
|
+
console.error("No blueprint selected for operation");
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
// Ensure selectedBlueprint is set in state if it wasn't already
|
|
210
|
+
if (!selectedBlueprint && blueprint) {
|
|
211
|
+
setSelectedBlueprint(blueprint);
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
setOperationLoading(true);
|
|
215
|
+
switch (operation) {
|
|
216
|
+
case "view_logs":
|
|
217
|
+
// Navigate to the logs screen
|
|
218
|
+
setOperationLoading(false);
|
|
219
|
+
setExecutingOperation(null);
|
|
220
|
+
navigate("blueprint-logs", {
|
|
221
|
+
blueprintId: blueprint.id,
|
|
222
|
+
blueprintName: blueprint.name || blueprint.id,
|
|
223
|
+
});
|
|
224
|
+
return;
|
|
225
|
+
case "create_devbox":
|
|
226
|
+
setShowCreateDevbox(true);
|
|
227
|
+
setExecutingOperation(null);
|
|
228
|
+
setOperationLoading(false);
|
|
229
|
+
return;
|
|
230
|
+
case "delete":
|
|
231
|
+
await client.blueprints.delete(blueprint.id);
|
|
232
|
+
setOperationResult(`Blueprint ${blueprint.id} deleted successfully`);
|
|
233
|
+
break;
|
|
89
234
|
}
|
|
90
|
-
|
|
91
|
-
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
setOperationError(err);
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
setOperationLoading(false);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
// Filter operations based on blueprint status
|
|
244
|
+
const operations = selectedBlueprint
|
|
245
|
+
? allOperations.filter((op) => {
|
|
246
|
+
const status = selectedBlueprint.status;
|
|
247
|
+
if (op.key === "create_devbox") {
|
|
248
|
+
return status === "build_complete";
|
|
92
249
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
// Handle input for all views
|
|
250
|
+
return true;
|
|
251
|
+
})
|
|
252
|
+
: allOperations;
|
|
253
|
+
// Handle input for all views
|
|
97
254
|
useInput((input, key) => {
|
|
98
|
-
// Handle Ctrl+C to force exit
|
|
99
|
-
if (key.ctrl && input === "c") {
|
|
100
|
-
process.stdout.write("\x1b[?1049l"); // Exit alternate screen
|
|
101
|
-
process.exit(130);
|
|
102
|
-
}
|
|
103
255
|
// Handle operation input mode
|
|
104
256
|
if (executingOperation && !operationResult && !operationError) {
|
|
257
|
+
// Allow escape/q to cancel any operation, even during loading
|
|
258
|
+
if (input === "q" || key.escape) {
|
|
259
|
+
setExecutingOperation(null);
|
|
260
|
+
setOperationInput("");
|
|
261
|
+
setOperationLoading(false);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
105
264
|
const currentOp = allOperations.find((op) => op.key === executingOperation);
|
|
106
265
|
if (currentOp?.needsInput) {
|
|
107
266
|
if (key.return) {
|
|
108
267
|
executeOperation();
|
|
109
268
|
}
|
|
110
|
-
else if (input === "q" || key.escape) {
|
|
111
|
-
console.clear();
|
|
112
|
-
setExecutingOperation(null);
|
|
113
|
-
setOperationInput("");
|
|
114
|
-
}
|
|
115
269
|
}
|
|
116
270
|
return;
|
|
117
271
|
}
|
|
118
272
|
// Handle operation result display
|
|
119
273
|
if (operationResult || operationError) {
|
|
120
274
|
if (input === "q" || key.escape || key.return) {
|
|
121
|
-
console.clear();
|
|
122
275
|
setOperationResult(null);
|
|
123
276
|
setOperationError(null);
|
|
124
277
|
setExecutingOperation(null);
|
|
@@ -128,9 +281,9 @@ const ListBlueprintsUI = ({ onBack, onExit }) => {
|
|
|
128
281
|
}
|
|
129
282
|
// Handle create devbox view
|
|
130
283
|
if (showCreateDevbox) {
|
|
131
|
-
return;
|
|
284
|
+
return;
|
|
132
285
|
}
|
|
133
|
-
// Handle actions popup overlay
|
|
286
|
+
// Handle actions popup overlay
|
|
134
287
|
if (showPopup) {
|
|
135
288
|
if (key.upArrow && selectedOperation > 0) {
|
|
136
289
|
setSelectedOperation(selectedOperation - 1);
|
|
@@ -140,66 +293,53 @@ const ListBlueprintsUI = ({ onBack, onExit }) => {
|
|
|
140
293
|
setSelectedOperation(selectedOperation + 1);
|
|
141
294
|
}
|
|
142
295
|
else if (key.return) {
|
|
143
|
-
console.clear();
|
|
144
296
|
setShowPopup(false);
|
|
145
297
|
const operationKey = allOperations[selectedOperation].key;
|
|
146
298
|
if (operationKey === "create_devbox") {
|
|
147
|
-
// Go directly to create devbox screen
|
|
148
299
|
setSelectedBlueprint(selectedBlueprintItem);
|
|
149
300
|
setShowCreateDevbox(true);
|
|
150
301
|
}
|
|
151
302
|
else {
|
|
152
|
-
// Execute other operations normally
|
|
153
303
|
setSelectedBlueprint(selectedBlueprintItem);
|
|
154
304
|
setExecutingOperation(operationKey);
|
|
155
|
-
executeOperation();
|
|
305
|
+
executeOperation(selectedBlueprintItem, operationKey);
|
|
156
306
|
}
|
|
157
307
|
}
|
|
158
308
|
else if (key.escape || input === "q") {
|
|
159
|
-
console.clear();
|
|
160
309
|
setShowPopup(false);
|
|
161
310
|
setSelectedOperation(0);
|
|
162
311
|
}
|
|
163
312
|
else if (input === "c") {
|
|
164
|
-
// Create devbox hotkey - only if blueprint is complete
|
|
165
313
|
if (selectedBlueprintItem &&
|
|
166
314
|
(selectedBlueprintItem.status === "build_complete" ||
|
|
167
315
|
selectedBlueprintItem.status === "building_complete")) {
|
|
168
|
-
console.clear();
|
|
169
316
|
setShowPopup(false);
|
|
170
317
|
setSelectedBlueprint(selectedBlueprintItem);
|
|
171
318
|
setShowCreateDevbox(true);
|
|
172
319
|
}
|
|
173
320
|
}
|
|
174
321
|
else if (input === "d") {
|
|
175
|
-
// Delete hotkey
|
|
176
322
|
const deleteIndex = allOperations.findIndex((op) => op.key === "delete");
|
|
177
323
|
if (deleteIndex >= 0) {
|
|
178
|
-
console.clear();
|
|
179
324
|
setShowPopup(false);
|
|
180
325
|
setSelectedBlueprint(selectedBlueprintItem);
|
|
181
326
|
setExecutingOperation("delete");
|
|
182
|
-
executeOperation();
|
|
327
|
+
executeOperation(selectedBlueprintItem, "delete");
|
|
183
328
|
}
|
|
184
329
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
330
|
+
else if (input === "l") {
|
|
331
|
+
const logsIndex = allOperations.findIndex((op) => op.key === "view_logs");
|
|
332
|
+
if (logsIndex >= 0) {
|
|
333
|
+
setShowPopup(false);
|
|
334
|
+
setSelectedBlueprint(selectedBlueprintItem);
|
|
335
|
+
setExecutingOperation("view_logs");
|
|
336
|
+
executeOperation(selectedBlueprintItem, "view_logs");
|
|
337
|
+
}
|
|
193
338
|
}
|
|
194
339
|
return;
|
|
195
340
|
}
|
|
196
|
-
// Handle list navigation
|
|
197
|
-
const
|
|
198
|
-
const totalPages = Math.ceil(blueprints.length / pageSize);
|
|
199
|
-
const startIndex = currentPage * pageSize;
|
|
200
|
-
const endIndex = Math.min(startIndex + pageSize, blueprints.length);
|
|
201
|
-
const currentBlueprints = blueprints.slice(startIndex, endIndex);
|
|
202
|
-
const pageBlueprints = currentBlueprints.length;
|
|
341
|
+
// Handle list navigation
|
|
342
|
+
const pageBlueprints = blueprints.length;
|
|
203
343
|
if (key.upArrow && selectedIndex > 0) {
|
|
204
344
|
setSelectedIndex(selectedIndex - 1);
|
|
205
345
|
}
|
|
@@ -207,22 +347,30 @@ const ListBlueprintsUI = ({ onBack, onExit }) => {
|
|
|
207
347
|
setSelectedIndex(selectedIndex + 1);
|
|
208
348
|
}
|
|
209
349
|
else if ((input === "n" || key.rightArrow) &&
|
|
210
|
-
|
|
211
|
-
|
|
350
|
+
!loading &&
|
|
351
|
+
!navigating &&
|
|
352
|
+
hasMore) {
|
|
353
|
+
nextPage();
|
|
212
354
|
setSelectedIndex(0);
|
|
213
355
|
}
|
|
214
|
-
else if ((input === "p" || key.leftArrow) &&
|
|
215
|
-
|
|
356
|
+
else if ((input === "p" || key.leftArrow) &&
|
|
357
|
+
!loading &&
|
|
358
|
+
!navigating &&
|
|
359
|
+
hasPrev) {
|
|
360
|
+
prevPage();
|
|
216
361
|
setSelectedIndex(0);
|
|
217
362
|
}
|
|
218
363
|
else if (input === "a") {
|
|
219
|
-
console.clear();
|
|
220
364
|
setShowPopup(true);
|
|
221
365
|
setSelectedOperation(0);
|
|
222
366
|
}
|
|
223
|
-
else if (input === "
|
|
224
|
-
|
|
225
|
-
|
|
367
|
+
else if (input === "l" && selectedBlueprintItem) {
|
|
368
|
+
setSelectedBlueprint(selectedBlueprintItem);
|
|
369
|
+
setExecutingOperation("view_logs");
|
|
370
|
+
executeOperation(selectedBlueprintItem, "view_logs");
|
|
371
|
+
}
|
|
372
|
+
else if (input === "o" && blueprints[selectedIndex]) {
|
|
373
|
+
const url = getBlueprintUrl(blueprints[selectedIndex].id);
|
|
226
374
|
const openBrowser = async () => {
|
|
227
375
|
const { exec } = await import("child_process");
|
|
228
376
|
const platform = process.platform;
|
|
@@ -247,63 +395,11 @@ const ListBlueprintsUI = ({ onBack, onExit }) => {
|
|
|
247
395
|
else if (onExit) {
|
|
248
396
|
onExit();
|
|
249
397
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
// Pagination computed early to allow hooks before any returns
|
|
253
|
-
const pageSize = PAGE_SIZE;
|
|
254
|
-
const totalPages = Math.ceil(blueprints.length / pageSize);
|
|
255
|
-
const startIndex = currentPage * pageSize;
|
|
256
|
-
const endIndex = Math.min(startIndex + pageSize, blueprints.length);
|
|
257
|
-
const currentBlueprints = blueprints.slice(startIndex, endIndex);
|
|
258
|
-
// Ensure selected index is within bounds - place before any returns
|
|
259
|
-
React.useEffect(() => {
|
|
260
|
-
if (currentBlueprints.length > 0 &&
|
|
261
|
-
selectedIndex >= currentBlueprints.length) {
|
|
262
|
-
setSelectedIndex(Math.max(0, currentBlueprints.length - 1));
|
|
263
|
-
}
|
|
264
|
-
}, [currentBlueprints.length, selectedIndex]);
|
|
265
|
-
const selectedBlueprintItem = currentBlueprints[selectedIndex];
|
|
266
|
-
// Generate operations based on selected blueprint status
|
|
267
|
-
const allOperations = getOperationsForBlueprint(selectedBlueprintItem);
|
|
268
|
-
const executeOperation = async () => {
|
|
269
|
-
const client = getClient();
|
|
270
|
-
const blueprint = selectedBlueprint;
|
|
271
|
-
if (!blueprint)
|
|
272
|
-
return;
|
|
273
|
-
try {
|
|
274
|
-
setLoading(true);
|
|
275
|
-
switch (executingOperation) {
|
|
276
|
-
case "create_devbox":
|
|
277
|
-
// Navigate to create devbox screen with blueprint pre-filled
|
|
278
|
-
setShowCreateDevbox(true);
|
|
279
|
-
setExecutingOperation(null);
|
|
280
|
-
setLoading(false);
|
|
281
|
-
return;
|
|
282
|
-
case "delete":
|
|
283
|
-
await client.blueprints.delete(blueprint.id);
|
|
284
|
-
setOperationResult(`Blueprint ${blueprint.id} deleted successfully`);
|
|
285
|
-
break;
|
|
398
|
+
else {
|
|
399
|
+
inkExit();
|
|
286
400
|
}
|
|
287
401
|
}
|
|
288
|
-
|
|
289
|
-
setOperationError(err);
|
|
290
|
-
}
|
|
291
|
-
finally {
|
|
292
|
-
setLoading(false);
|
|
293
|
-
}
|
|
294
|
-
};
|
|
295
|
-
// Filter operations based on blueprint status
|
|
296
|
-
const operations = selectedBlueprint
|
|
297
|
-
? allOperations.filter((op) => {
|
|
298
|
-
const status = selectedBlueprint.status;
|
|
299
|
-
// Only allow creating devbox if build is complete
|
|
300
|
-
if (op.key === "create_devbox") {
|
|
301
|
-
return status === "build_complete";
|
|
302
|
-
}
|
|
303
|
-
// Allow delete for any status
|
|
304
|
-
return true;
|
|
305
|
-
})
|
|
306
|
-
: allOperations;
|
|
402
|
+
});
|
|
307
403
|
// Operation result display
|
|
308
404
|
if (operationResult || operationError) {
|
|
309
405
|
const operationLabel = operations.find((o) => o.key === executingOperation)?.label ||
|
|
@@ -321,113 +417,52 @@ const ListBlueprintsUI = ({ onBack, onExit }) => {
|
|
|
321
417
|
const currentOp = allOperations.find((op) => op.key === executingOperation);
|
|
322
418
|
const needsInput = currentOp?.needsInput;
|
|
323
419
|
const operationLabel = currentOp?.label || "Operation";
|
|
324
|
-
if (
|
|
420
|
+
if (operationLoading) {
|
|
421
|
+
const messages = {
|
|
422
|
+
delete: "Deleting blueprint...",
|
|
423
|
+
view_logs: "Fetching logs...",
|
|
424
|
+
};
|
|
325
425
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
326
426
|
{ label: "Blueprints" },
|
|
327
427
|
{ label: selectedBlueprint.name || selectedBlueprint.id },
|
|
328
428
|
{ label: operationLabel, active: true },
|
|
329
|
-
] }), _jsx(Header, { title: "Executing Operation" }), _jsx(SpinnerComponent, { message: "Please wait..." })] }));
|
|
429
|
+
] }), _jsx(Header, { title: "Executing Operation" }), _jsx(SpinnerComponent, { message: messages[executingOperation] || "Please wait..." }), _jsx(Box, { marginTop: 1, paddingX: 1, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: "Press [q] or [esc] to cancel" }) })] }));
|
|
330
430
|
}
|
|
331
|
-
if
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
};
|
|
431
|
+
// Only show input screen if operation needs input
|
|
432
|
+
// Operations like view_logs navigate away and don't need this screen
|
|
433
|
+
if (needsInput) {
|
|
335
434
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
336
435
|
{ label: "Blueprints" },
|
|
337
436
|
{ label: selectedBlueprint.name || selectedBlueprint.id },
|
|
338
437
|
{ label: operationLabel, active: true },
|
|
339
|
-
] }), _jsx(Header, { title:
|
|
438
|
+
] }), _jsx(Header, { title: operationLabel }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: colors.primary, bold: true, children: selectedBlueprint.name || selectedBlueprint.id }) }), _jsx(Box, { children: _jsxs(Text, { color: colors.textDim, children: [currentOp?.inputPrompt || "", " "] }) }), _jsx(Box, { marginTop: 1, children: _jsx(TextInput, { value: operationInput, onChange: setOperationInput, placeholder: currentOp?.inputPlaceholder || "" }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: "Press [Enter] to execute \u2022 [q or esc] Cancel" }) })] })] }));
|
|
340
439
|
}
|
|
341
|
-
|
|
342
|
-
{ label: "Blueprints" },
|
|
343
|
-
{ label: selectedBlueprint.name || selectedBlueprint.id },
|
|
344
|
-
{ label: operationLabel, active: true },
|
|
345
|
-
] }), _jsx(Header, { title: operationLabel }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: colors.primary, bold: true, children: selectedBlueprint.name || selectedBlueprint.id }) }), _jsx(Box, { children: _jsxs(Text, { color: colors.textDim, children: [currentOp.inputPrompt, " "] }) }), _jsx(Box, { marginTop: 1, children: _jsx(TextInput, { value: operationInput, onChange: setOperationInput, placeholder: currentOp.inputPlaceholder || "" }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: "Press [Enter] to execute \u2022 [q or esc] Cancel" }) })] })] }));
|
|
440
|
+
// For operations that don't need input (like view_logs), fall through to list view
|
|
346
441
|
}
|
|
347
442
|
// Create devbox screen
|
|
348
443
|
if (showCreateDevbox && selectedBlueprint) {
|
|
349
444
|
return (_jsx(DevboxCreatePage, { onBack: () => {
|
|
350
445
|
setShowCreateDevbox(false);
|
|
351
446
|
setSelectedBlueprint(null);
|
|
352
|
-
}, onCreate: (
|
|
353
|
-
// Return to blueprint list after creation
|
|
447
|
+
}, onCreate: () => {
|
|
354
448
|
setShowCreateDevbox(false);
|
|
355
449
|
setSelectedBlueprint(null);
|
|
356
450
|
}, initialBlueprintId: selectedBlueprint.id }));
|
|
357
451
|
}
|
|
358
452
|
// Loading state
|
|
359
|
-
if (loading) {
|
|
360
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Blueprints", active: true }]
|
|
453
|
+
if (loading && blueprints.length === 0) {
|
|
454
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Blueprints", active: true }] }), _jsx(SpinnerComponent, { message: "Loading blueprints..." })] }));
|
|
361
455
|
}
|
|
362
456
|
// Error state
|
|
363
457
|
if (listError) {
|
|
364
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Blueprints", active: true }]
|
|
458
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Blueprints", active: true }] }), _jsx(ErrorMessage, { message: "Failed to load blueprints", error: listError })] }));
|
|
365
459
|
}
|
|
366
460
|
// Empty state
|
|
367
461
|
if (blueprints.length === 0) {
|
|
368
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Blueprints", active: true }]
|
|
462
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Blueprints", active: true }] }), _jsxs(Box, { children: [_jsx(Text, { color: colors.warning, children: figures.info }), _jsx(Text, { children: " No blueprints found. Try: " }), _jsx(Text, { color: colors.primary, bold: true, children: "rli blueprint create" })] })] }));
|
|
369
463
|
}
|
|
370
|
-
// Pagination moved earlier
|
|
371
|
-
// Overlay: draw quick actions popup over the table (keep table visible)
|
|
372
464
|
// List view
|
|
373
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Blueprints", active: true }] }), _jsx(Table, { data:
|
|
374
|
-
{
|
|
375
|
-
key: "statusIcon",
|
|
376
|
-
label: "",
|
|
377
|
-
width: statusIconWidth,
|
|
378
|
-
render: (blueprint, index, isSelected) => {
|
|
379
|
-
const statusDisplay = getStatusDisplay(blueprint.status);
|
|
380
|
-
const statusColor = statusDisplay.color === colors.textDim
|
|
381
|
-
? colors.info
|
|
382
|
-
: statusDisplay.color;
|
|
383
|
-
return (_jsxs(Text, { color: isSelected ? "white" : statusColor, bold: true, dimColor: false, inverse: isSelected, wrap: "truncate", children: [statusDisplay.icon, " "] }));
|
|
384
|
-
},
|
|
385
|
-
},
|
|
386
|
-
{
|
|
387
|
-
key: "id",
|
|
388
|
-
label: "ID",
|
|
389
|
-
width: idWidth + 1,
|
|
390
|
-
render: (blueprint, index, isSelected) => {
|
|
391
|
-
const value = blueprint.id;
|
|
392
|
-
const width = idWidth + 1;
|
|
393
|
-
const truncated = value.slice(0, width - 1);
|
|
394
|
-
const padded = truncated.padEnd(width, " ");
|
|
395
|
-
return (_jsx(Text, { color: isSelected ? "white" : colors.textDim, bold: false, dimColor: false, inverse: isSelected, wrap: "truncate", children: padded }));
|
|
396
|
-
},
|
|
397
|
-
},
|
|
398
|
-
{
|
|
399
|
-
key: "statusText",
|
|
400
|
-
label: "Status",
|
|
401
|
-
width: statusTextWidth,
|
|
402
|
-
render: (blueprint, index, isSelected) => {
|
|
403
|
-
const statusDisplay = getStatusDisplay(blueprint.status);
|
|
404
|
-
const statusColor = statusDisplay.color === colors.textDim
|
|
405
|
-
? colors.info
|
|
406
|
-
: statusDisplay.color;
|
|
407
|
-
const truncated = statusDisplay.text.slice(0, statusTextWidth);
|
|
408
|
-
const padded = truncated.padEnd(statusTextWidth, " ");
|
|
409
|
-
return (_jsx(Text, { color: isSelected ? "white" : statusColor, bold: true, dimColor: false, inverse: isSelected, wrap: "truncate", children: padded }));
|
|
410
|
-
},
|
|
411
|
-
},
|
|
412
|
-
createTextColumn("name", "Name", (blueprint) => blueprint.name || "(unnamed)", {
|
|
413
|
-
width: nameWidth,
|
|
414
|
-
}),
|
|
415
|
-
createTextColumn("description", "Description", (blueprint) => blueprint.dockerfile_setup?.description || "", {
|
|
416
|
-
width: descriptionWidth,
|
|
417
|
-
color: colors.textDim,
|
|
418
|
-
dimColor: false,
|
|
419
|
-
bold: false,
|
|
420
|
-
visible: showDescription,
|
|
421
|
-
}),
|
|
422
|
-
createTextColumn("created", "Created", (blueprint) => blueprint.create_time_ms
|
|
423
|
-
? formatTimeAgo(blueprint.create_time_ms)
|
|
424
|
-
: "", {
|
|
425
|
-
width: timeWidth,
|
|
426
|
-
color: colors.textDim,
|
|
427
|
-
dimColor: false,
|
|
428
|
-
bold: false,
|
|
429
|
-
}),
|
|
430
|
-
] }), _jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", blueprints.length] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "total"] }), totalPages > 1 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Page ", currentPage + 1, " of ", totalPages] })] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Showing ", startIndex + 1, "-", endIndex, " of ", blueprints.length] })] }), showPopup && selectedBlueprintItem && (_jsx(Box, { marginTop: -Math.min(allOperations.length + 10, PAGE_SIZE + 5), justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedBlueprintItem, operations: allOperations.map((op) => ({
|
|
465
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Blueprints", active: true }] }), !showPopup && (_jsx(Table, { data: blueprints, keyExtractor: (blueprint) => blueprint.id, selectedIndex: selectedIndex, title: `blueprints[${totalCount}]`, columns: blueprintColumns })), !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] })] })), showPopup && selectedBlueprintItem && (_jsx(Box, { marginTop: 2, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedBlueprintItem, operations: allOperations.map((op) => ({
|
|
431
466
|
key: op.key,
|
|
432
467
|
label: op.label,
|
|
433
468
|
color: op.color,
|
|
@@ -436,17 +471,30 @@ const ListBlueprintsUI = ({ onBack, onExit }) => {
|
|
|
436
471
|
? "c"
|
|
437
472
|
: op.key === "delete"
|
|
438
473
|
? "d"
|
|
439
|
-
: ""
|
|
440
|
-
|
|
474
|
+
: op.key === "view_logs"
|
|
475
|
+
? "l"
|
|
476
|
+
: "",
|
|
477
|
+
})), selectedOperation: selectedOperation, onClose: () => setShowPopup(false) }) })), _jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate"] }), (hasMore || hasPrev) && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 ", figures.arrowLeft, figures.arrowRight, " Page"] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 [a] Actions"] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 [o] Browser"] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 [Esc] Back"] })] })] }));
|
|
441
478
|
};
|
|
442
479
|
// Export the UI component for use in the main menu
|
|
443
480
|
export { ListBlueprintsUI };
|
|
444
481
|
export async function listBlueprints(options = {}) {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
limit:
|
|
450
|
-
}
|
|
451
|
-
|
|
482
|
+
try {
|
|
483
|
+
const client = getClient();
|
|
484
|
+
// Build query params
|
|
485
|
+
const queryParams = {
|
|
486
|
+
limit: DEFAULT_PAGE_SIZE,
|
|
487
|
+
};
|
|
488
|
+
if (options.name) {
|
|
489
|
+
queryParams.name = options.name;
|
|
490
|
+
}
|
|
491
|
+
// Fetch blueprints
|
|
492
|
+
const page = (await client.blueprints.list(queryParams));
|
|
493
|
+
// Extract blueprints array
|
|
494
|
+
const blueprints = page.blueprints || [];
|
|
495
|
+
output(blueprints, { format: options.output, defaultFormat: "json" });
|
|
496
|
+
}
|
|
497
|
+
catch (error) {
|
|
498
|
+
outputError("Failed to list blueprints", error);
|
|
499
|
+
}
|
|
452
500
|
}
|