@runloop/rl-cli 0.1.1 → 0.2.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 -0
- package/dist/cli.js +73 -60
- package/dist/commands/auth.js +0 -1
- package/dist/commands/blueprint/create.js +31 -83
- package/dist/commands/blueprint/get.js +29 -34
- package/dist/commands/blueprint/list.js +215 -213
- package/dist/commands/blueprint/logs.js +133 -37
- package/dist/commands/blueprint/preview.js +42 -38
- package/dist/commands/config.js +117 -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 +45 -78
- package/dist/commands/devbox/suspend.js +13 -35
- package/dist/commands/devbox/tunnel.js +36 -88
- package/dist/commands/devbox/upload.js +28 -36
- package/dist/commands/devbox/write.js +29 -44
- package/dist/commands/mcp-install.js +4 -3
- package/dist/commands/menu.js +24 -66
- 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 +63 -39
- package/dist/components/Breadcrumb.js +10 -52
- package/dist/components/DevboxActionsMenu.js +182 -110
- 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 +94 -0
- package/dist/components/MainMenu.js +36 -32
- 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/hooks/useCursorPagination.js +125 -85
- package/dist/hooks/useExitOnCtrlC.js +14 -0
- package/dist/hooks/useViewportHeight.js +47 -0
- package/dist/mcp/server.js +65 -6
- package/dist/router/Router.js +68 -0
- package/dist/router/types.js +1 -0
- package/dist/screens/BlueprintListScreen.js +7 -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 +105 -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/CommandExecutor.js +53 -24
- package/dist/utils/client.js +0 -2
- package/dist/utils/config.js +20 -90
- package/dist/utils/interactiveCommand.js +3 -2
- package/dist/utils/logFormatter.js +162 -0
- package/dist/utils/memoryMonitor.js +85 -0
- package/dist/utils/output.js +150 -59
- package/dist/utils/screen.js +23 -0
- package/dist/utils/ssh.js +3 -1
- package/dist/utils/sshSession.js +5 -29
- package/dist/utils/terminalDetection.js +97 -0
- package/dist/utils/terminalSync.js +39 -0
- package/dist/utils/theme.js +147 -13
- package/package.json +16 -13
|
@@ -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,148 @@ 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
|
+
const DEFAULT_PAGE_SIZE = 10;
|
|
24
|
+
const ListBlueprintsUI = ({ onBack, onExit, }) => {
|
|
25
|
+
const { exit: inkExit } = useApp();
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
24
27
|
const [selectedBlueprint, setSelectedBlueprint] = React.useState(null);
|
|
25
28
|
const [selectedOperation, setSelectedOperation] = React.useState(0);
|
|
26
29
|
const [executingOperation, setExecutingOperation] = React.useState(null);
|
|
27
30
|
const [operationInput, setOperationInput] = React.useState("");
|
|
28
31
|
const [operationResult, setOperationResult] = React.useState(null);
|
|
29
32
|
const [operationError, setOperationError] = React.useState(null);
|
|
30
|
-
const [
|
|
33
|
+
const [operationLoading, setOperationLoading] = React.useState(false);
|
|
31
34
|
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
35
|
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
|
37
|
-
const [showActions, setShowActions] = React.useState(false);
|
|
38
36
|
const [showPopup, setShowPopup] = React.useState(false);
|
|
39
|
-
// Calculate
|
|
40
|
-
const
|
|
41
|
-
const
|
|
37
|
+
// Calculate overhead for viewport height
|
|
38
|
+
const overhead = 13;
|
|
39
|
+
const { viewportHeight, terminalWidth } = useViewportHeight({
|
|
40
|
+
overhead,
|
|
41
|
+
minHeight: 5,
|
|
42
|
+
});
|
|
43
|
+
const PAGE_SIZE = viewportHeight;
|
|
44
|
+
// All width constants
|
|
42
45
|
const statusIconWidth = 2;
|
|
43
46
|
const statusTextWidth = 10;
|
|
44
47
|
const idWidth = 25;
|
|
45
|
-
const nameWidth = terminalWidth >= 120 ? 30 : 25;
|
|
48
|
+
const nameWidth = Math.max(15, terminalWidth >= 120 ? 30 : 25);
|
|
46
49
|
const descriptionWidth = 40;
|
|
47
50
|
const timeWidth = 20;
|
|
51
|
+
const showDescription = terminalWidth >= 120;
|
|
52
|
+
// Fetch function for pagination hook
|
|
53
|
+
const fetchPage = React.useCallback(async (params) => {
|
|
54
|
+
const client = getClient();
|
|
55
|
+
const pageBlueprints = [];
|
|
56
|
+
// Build query params
|
|
57
|
+
const queryParams = {
|
|
58
|
+
limit: params.limit,
|
|
59
|
+
};
|
|
60
|
+
if (params.startingAt) {
|
|
61
|
+
queryParams.starting_after = params.startingAt;
|
|
62
|
+
}
|
|
63
|
+
// Fetch ONE page only
|
|
64
|
+
const page = (await client.blueprints.list(queryParams));
|
|
65
|
+
// Extract data and create defensive copies
|
|
66
|
+
if (page.blueprints && Array.isArray(page.blueprints)) {
|
|
67
|
+
page.blueprints.forEach((b) => {
|
|
68
|
+
pageBlueprints.push({
|
|
69
|
+
id: b.id,
|
|
70
|
+
name: b.name,
|
|
71
|
+
status: b.status,
|
|
72
|
+
create_time_ms: b.create_time_ms,
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
const result = {
|
|
77
|
+
items: pageBlueprints,
|
|
78
|
+
hasMore: page.has_more || false,
|
|
79
|
+
totalCount: page.total_count || pageBlueprints.length,
|
|
80
|
+
};
|
|
81
|
+
return result;
|
|
82
|
+
}, []);
|
|
83
|
+
// Use the shared pagination hook
|
|
84
|
+
const { items: blueprints, loading, navigating, error: listError, currentPage, hasMore, hasPrev, totalCount, nextPage, prevPage, } = useCursorPagination({
|
|
85
|
+
fetchPage,
|
|
86
|
+
pageSize: PAGE_SIZE,
|
|
87
|
+
getItemId: (blueprint) => blueprint.id,
|
|
88
|
+
pollInterval: 2000,
|
|
89
|
+
pollingEnabled: !showPopup && !showCreateDevbox && !executingOperation,
|
|
90
|
+
deps: [PAGE_SIZE],
|
|
91
|
+
});
|
|
92
|
+
// Memoize columns array
|
|
93
|
+
const blueprintColumns = React.useMemo(() => [
|
|
94
|
+
{
|
|
95
|
+
key: "statusIcon",
|
|
96
|
+
label: "",
|
|
97
|
+
width: statusIconWidth,
|
|
98
|
+
render: (blueprint, _index, isSelected) => {
|
|
99
|
+
const statusDisplay = getStatusDisplay(blueprint.status || "");
|
|
100
|
+
const statusColor = statusDisplay.color === colors.textDim
|
|
101
|
+
? colors.info
|
|
102
|
+
: statusDisplay.color;
|
|
103
|
+
return (_jsxs(Text, { color: isSelected ? "white" : statusColor, bold: true, dimColor: false, inverse: isSelected, wrap: "truncate", children: [statusDisplay.icon, " "] }));
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
key: "id",
|
|
108
|
+
label: "ID",
|
|
109
|
+
width: idWidth + 1,
|
|
110
|
+
render: (blueprint, _index, isSelected) => {
|
|
111
|
+
const value = blueprint.id;
|
|
112
|
+
const width = Math.max(1, idWidth + 1);
|
|
113
|
+
const truncated = value.slice(0, Math.max(1, width - 1));
|
|
114
|
+
const padded = truncated.padEnd(width, " ");
|
|
115
|
+
return (_jsx(Text, { color: isSelected ? "white" : colors.idColor, bold: false, dimColor: false, inverse: isSelected, wrap: "truncate", children: padded }));
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
key: "statusText",
|
|
120
|
+
label: "Status",
|
|
121
|
+
width: statusTextWidth,
|
|
122
|
+
render: (blueprint, _index, isSelected) => {
|
|
123
|
+
const statusDisplay = getStatusDisplay(blueprint.status || "");
|
|
124
|
+
const statusColor = statusDisplay.color === colors.textDim
|
|
125
|
+
? colors.info
|
|
126
|
+
: statusDisplay.color;
|
|
127
|
+
const safeWidth = Math.max(1, statusTextWidth);
|
|
128
|
+
const truncated = statusDisplay.text.slice(0, safeWidth);
|
|
129
|
+
const padded = truncated.padEnd(safeWidth, " ");
|
|
130
|
+
return (_jsx(Text, { color: isSelected ? "white" : statusColor, bold: true, dimColor: false, inverse: isSelected, wrap: "truncate", children: padded }));
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
createTextColumn("name", "Name", (blueprint) => blueprint.name || "", {
|
|
134
|
+
width: nameWidth,
|
|
135
|
+
}),
|
|
136
|
+
// Description column removed - not available in API
|
|
137
|
+
createTextColumn("created", "Created", (blueprint) => blueprint.create_time_ms
|
|
138
|
+
? formatTimeAgo(blueprint.create_time_ms)
|
|
139
|
+
: "", {
|
|
140
|
+
width: timeWidth,
|
|
141
|
+
color: colors.textDim,
|
|
142
|
+
dimColor: false,
|
|
143
|
+
bold: false,
|
|
144
|
+
}),
|
|
145
|
+
], [
|
|
146
|
+
statusIconWidth,
|
|
147
|
+
statusTextWidth,
|
|
148
|
+
idWidth,
|
|
149
|
+
nameWidth,
|
|
150
|
+
descriptionWidth,
|
|
151
|
+
timeWidth,
|
|
152
|
+
showDescription,
|
|
153
|
+
]);
|
|
48
154
|
// Helper function to generate operations based on selected blueprint
|
|
49
155
|
const getOperationsForBlueprint = (blueprint) => {
|
|
50
156
|
const operations = [];
|
|
51
|
-
// Only show create devbox option if blueprint is successfully built
|
|
52
157
|
if (blueprint &&
|
|
53
158
|
(blueprint.status === "build_complete" ||
|
|
54
159
|
blueprint.status === "building_complete")) {
|
|
@@ -59,7 +164,6 @@ const ListBlueprintsUI = ({ onBack, onExit }) => {
|
|
|
59
164
|
icon: figures.play,
|
|
60
165
|
});
|
|
61
166
|
}
|
|
62
|
-
// Always show delete option
|
|
63
167
|
operations.push({
|
|
64
168
|
key: "delete",
|
|
65
169
|
label: "Delete Blueprint",
|
|
@@ -68,38 +172,58 @@ const ListBlueprintsUI = ({ onBack, onExit }) => {
|
|
|
68
172
|
});
|
|
69
173
|
return operations;
|
|
70
174
|
};
|
|
71
|
-
//
|
|
175
|
+
// Handle Ctrl+C to exit
|
|
176
|
+
useExitOnCtrlC();
|
|
177
|
+
// Ensure selected index is within bounds
|
|
72
178
|
React.useEffect(() => {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
179
|
+
if (blueprints.length > 0 && selectedIndex >= blueprints.length) {
|
|
180
|
+
setSelectedIndex(Math.max(0, blueprints.length - 1));
|
|
181
|
+
}
|
|
182
|
+
}, [blueprints.length, selectedIndex]);
|
|
183
|
+
const selectedBlueprintItem = blueprints[selectedIndex];
|
|
184
|
+
const allOperations = getOperationsForBlueprint(selectedBlueprintItem);
|
|
185
|
+
// Calculate pagination info for display
|
|
186
|
+
const totalPages = Math.max(1, Math.ceil(totalCount / PAGE_SIZE));
|
|
187
|
+
const startIndex = currentPage * PAGE_SIZE;
|
|
188
|
+
const endIndex = startIndex + blueprints.length;
|
|
189
|
+
const executeOperation = async () => {
|
|
190
|
+
const client = getClient();
|
|
191
|
+
const blueprint = selectedBlueprint;
|
|
192
|
+
if (!blueprint)
|
|
193
|
+
return;
|
|
194
|
+
try {
|
|
195
|
+
setOperationLoading(true);
|
|
196
|
+
switch (executingOperation) {
|
|
197
|
+
case "create_devbox":
|
|
198
|
+
setShowCreateDevbox(true);
|
|
199
|
+
setExecutingOperation(null);
|
|
200
|
+
setOperationLoading(false);
|
|
201
|
+
return;
|
|
202
|
+
case "delete":
|
|
203
|
+
await client.blueprints.delete(blueprint.id);
|
|
204
|
+
setOperationResult(`Blueprint ${blueprint.id} deleted successfully`);
|
|
205
|
+
break;
|
|
89
206
|
}
|
|
90
|
-
|
|
91
|
-
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
setOperationError(err);
|
|
210
|
+
}
|
|
211
|
+
finally {
|
|
212
|
+
setOperationLoading(false);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
// Filter operations based on blueprint status
|
|
216
|
+
const operations = selectedBlueprint
|
|
217
|
+
? allOperations.filter((op) => {
|
|
218
|
+
const status = selectedBlueprint.status;
|
|
219
|
+
if (op.key === "create_devbox") {
|
|
220
|
+
return status === "build_complete";
|
|
92
221
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
// Handle input for all views
|
|
222
|
+
return true;
|
|
223
|
+
})
|
|
224
|
+
: allOperations;
|
|
225
|
+
// Handle input for all views
|
|
97
226
|
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
227
|
// Handle operation input mode
|
|
104
228
|
if (executingOperation && !operationResult && !operationError) {
|
|
105
229
|
const currentOp = allOperations.find((op) => op.key === executingOperation);
|
|
@@ -108,7 +232,6 @@ const ListBlueprintsUI = ({ onBack, onExit }) => {
|
|
|
108
232
|
executeOperation();
|
|
109
233
|
}
|
|
110
234
|
else if (input === "q" || key.escape) {
|
|
111
|
-
console.clear();
|
|
112
235
|
setExecutingOperation(null);
|
|
113
236
|
setOperationInput("");
|
|
114
237
|
}
|
|
@@ -118,7 +241,6 @@ const ListBlueprintsUI = ({ onBack, onExit }) => {
|
|
|
118
241
|
// Handle operation result display
|
|
119
242
|
if (operationResult || operationError) {
|
|
120
243
|
if (input === "q" || key.escape || key.return) {
|
|
121
|
-
console.clear();
|
|
122
244
|
setOperationResult(null);
|
|
123
245
|
setOperationError(null);
|
|
124
246
|
setExecutingOperation(null);
|
|
@@ -128,9 +250,9 @@ const ListBlueprintsUI = ({ onBack, onExit }) => {
|
|
|
128
250
|
}
|
|
129
251
|
// Handle create devbox view
|
|
130
252
|
if (showCreateDevbox) {
|
|
131
|
-
return;
|
|
253
|
+
return;
|
|
132
254
|
}
|
|
133
|
-
// Handle actions popup overlay
|
|
255
|
+
// Handle actions popup overlay
|
|
134
256
|
if (showPopup) {
|
|
135
257
|
if (key.upArrow && selectedOperation > 0) {
|
|
136
258
|
setSelectedOperation(selectedOperation - 1);
|
|
@@ -140,66 +262,44 @@ const ListBlueprintsUI = ({ onBack, onExit }) => {
|
|
|
140
262
|
setSelectedOperation(selectedOperation + 1);
|
|
141
263
|
}
|
|
142
264
|
else if (key.return) {
|
|
143
|
-
console.clear();
|
|
144
265
|
setShowPopup(false);
|
|
145
266
|
const operationKey = allOperations[selectedOperation].key;
|
|
146
267
|
if (operationKey === "create_devbox") {
|
|
147
|
-
// Go directly to create devbox screen
|
|
148
268
|
setSelectedBlueprint(selectedBlueprintItem);
|
|
149
269
|
setShowCreateDevbox(true);
|
|
150
270
|
}
|
|
151
271
|
else {
|
|
152
|
-
// Execute other operations normally
|
|
153
272
|
setSelectedBlueprint(selectedBlueprintItem);
|
|
154
273
|
setExecutingOperation(operationKey);
|
|
155
274
|
executeOperation();
|
|
156
275
|
}
|
|
157
276
|
}
|
|
158
277
|
else if (key.escape || input === "q") {
|
|
159
|
-
console.clear();
|
|
160
278
|
setShowPopup(false);
|
|
161
279
|
setSelectedOperation(0);
|
|
162
280
|
}
|
|
163
281
|
else if (input === "c") {
|
|
164
|
-
// Create devbox hotkey - only if blueprint is complete
|
|
165
282
|
if (selectedBlueprintItem &&
|
|
166
283
|
(selectedBlueprintItem.status === "build_complete" ||
|
|
167
284
|
selectedBlueprintItem.status === "building_complete")) {
|
|
168
|
-
console.clear();
|
|
169
285
|
setShowPopup(false);
|
|
170
286
|
setSelectedBlueprint(selectedBlueprintItem);
|
|
171
287
|
setShowCreateDevbox(true);
|
|
172
288
|
}
|
|
173
289
|
}
|
|
174
290
|
else if (input === "d") {
|
|
175
|
-
// Delete hotkey
|
|
176
291
|
const deleteIndex = allOperations.findIndex((op) => op.key === "delete");
|
|
177
292
|
if (deleteIndex >= 0) {
|
|
178
|
-
console.clear();
|
|
179
293
|
setShowPopup(false);
|
|
180
294
|
setSelectedBlueprint(selectedBlueprintItem);
|
|
181
295
|
setExecutingOperation("delete");
|
|
182
296
|
executeOperation();
|
|
183
297
|
}
|
|
184
298
|
}
|
|
185
|
-
return; // prevent falling through to list nav
|
|
186
|
-
}
|
|
187
|
-
// Handle actions view
|
|
188
|
-
if (showActions) {
|
|
189
|
-
if (input === "q" || key.escape) {
|
|
190
|
-
console.clear();
|
|
191
|
-
setShowActions(false);
|
|
192
|
-
setSelectedOperation(0);
|
|
193
|
-
}
|
|
194
299
|
return;
|
|
195
300
|
}
|
|
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;
|
|
301
|
+
// Handle list navigation
|
|
302
|
+
const pageBlueprints = blueprints.length;
|
|
203
303
|
if (key.upArrow && selectedIndex > 0) {
|
|
204
304
|
setSelectedIndex(selectedIndex - 1);
|
|
205
305
|
}
|
|
@@ -207,22 +307,25 @@ const ListBlueprintsUI = ({ onBack, onExit }) => {
|
|
|
207
307
|
setSelectedIndex(selectedIndex + 1);
|
|
208
308
|
}
|
|
209
309
|
else if ((input === "n" || key.rightArrow) &&
|
|
210
|
-
|
|
211
|
-
|
|
310
|
+
!loading &&
|
|
311
|
+
!navigating &&
|
|
312
|
+
hasMore) {
|
|
313
|
+
nextPage();
|
|
212
314
|
setSelectedIndex(0);
|
|
213
315
|
}
|
|
214
|
-
else if ((input === "p" || key.leftArrow) &&
|
|
215
|
-
|
|
316
|
+
else if ((input === "p" || key.leftArrow) &&
|
|
317
|
+
!loading &&
|
|
318
|
+
!navigating &&
|
|
319
|
+
hasPrev) {
|
|
320
|
+
prevPage();
|
|
216
321
|
setSelectedIndex(0);
|
|
217
322
|
}
|
|
218
323
|
else if (input === "a") {
|
|
219
|
-
console.clear();
|
|
220
324
|
setShowPopup(true);
|
|
221
325
|
setSelectedOperation(0);
|
|
222
326
|
}
|
|
223
|
-
else if (input === "o" &&
|
|
224
|
-
|
|
225
|
-
const url = getBlueprintUrl(currentBlueprints[selectedIndex].id);
|
|
327
|
+
else if (input === "o" && blueprints[selectedIndex]) {
|
|
328
|
+
const url = getBlueprintUrl(blueprints[selectedIndex].id);
|
|
226
329
|
const openBrowser = async () => {
|
|
227
330
|
const { exec } = await import("child_process");
|
|
228
331
|
const platform = process.platform;
|
|
@@ -247,63 +350,11 @@ const ListBlueprintsUI = ({ onBack, onExit }) => {
|
|
|
247
350
|
else if (onExit) {
|
|
248
351
|
onExit();
|
|
249
352
|
}
|
|
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;
|
|
353
|
+
else {
|
|
354
|
+
inkExit();
|
|
286
355
|
}
|
|
287
356
|
}
|
|
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;
|
|
357
|
+
});
|
|
307
358
|
// Operation result display
|
|
308
359
|
if (operationResult || operationError) {
|
|
309
360
|
const operationLabel = operations.find((o) => o.key === executingOperation)?.label ||
|
|
@@ -321,7 +372,7 @@ const ListBlueprintsUI = ({ onBack, onExit }) => {
|
|
|
321
372
|
const currentOp = allOperations.find((op) => op.key === executingOperation);
|
|
322
373
|
const needsInput = currentOp?.needsInput;
|
|
323
374
|
const operationLabel = currentOp?.label || "Operation";
|
|
324
|
-
if (
|
|
375
|
+
if (operationLoading) {
|
|
325
376
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
326
377
|
{ label: "Blueprints" },
|
|
327
378
|
{ label: selectedBlueprint.name || selectedBlueprint.id },
|
|
@@ -349,85 +400,25 @@ const ListBlueprintsUI = ({ onBack, onExit }) => {
|
|
|
349
400
|
return (_jsx(DevboxCreatePage, { onBack: () => {
|
|
350
401
|
setShowCreateDevbox(false);
|
|
351
402
|
setSelectedBlueprint(null);
|
|
352
|
-
}, onCreate: (
|
|
353
|
-
// Return to blueprint list after creation
|
|
403
|
+
}, onCreate: () => {
|
|
354
404
|
setShowCreateDevbox(false);
|
|
355
405
|
setSelectedBlueprint(null);
|
|
356
406
|
}, initialBlueprintId: selectedBlueprint.id }));
|
|
357
407
|
}
|
|
358
408
|
// Loading state
|
|
359
|
-
if (loading) {
|
|
360
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Blueprints", active: true }]
|
|
409
|
+
if (loading && blueprints.length === 0) {
|
|
410
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Blueprints", active: true }] }), _jsx(SpinnerComponent, { message: "Loading blueprints..." })] }));
|
|
361
411
|
}
|
|
362
412
|
// Error state
|
|
363
413
|
if (listError) {
|
|
364
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Blueprints", active: true }]
|
|
414
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Blueprints", active: true }] }), _jsx(ErrorMessage, { message: "Failed to load blueprints", error: listError })] }));
|
|
365
415
|
}
|
|
366
416
|
// Empty state
|
|
367
417
|
if (blueprints.length === 0) {
|
|
368
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Blueprints", active: true }]
|
|
418
|
+
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
419
|
}
|
|
370
|
-
// Pagination moved earlier
|
|
371
|
-
// Overlay: draw quick actions popup over the table (keep table visible)
|
|
372
420
|
// 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) => ({
|
|
421
|
+
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
422
|
key: op.key,
|
|
432
423
|
label: op.label,
|
|
433
424
|
color: op.color,
|
|
@@ -437,16 +428,27 @@ const ListBlueprintsUI = ({ onBack, onExit }) => {
|
|
|
437
428
|
: op.key === "delete"
|
|
438
429
|
? "d"
|
|
439
430
|
: "",
|
|
440
|
-
})), 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"] }),
|
|
431
|
+
})), 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
432
|
};
|
|
442
433
|
// Export the UI component for use in the main menu
|
|
443
434
|
export { ListBlueprintsUI };
|
|
444
435
|
export async function listBlueprints(options = {}) {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
limit:
|
|
450
|
-
}
|
|
451
|
-
|
|
436
|
+
try {
|
|
437
|
+
const client = getClient();
|
|
438
|
+
// Build query params
|
|
439
|
+
const queryParams = {
|
|
440
|
+
limit: DEFAULT_PAGE_SIZE,
|
|
441
|
+
};
|
|
442
|
+
if (options.name) {
|
|
443
|
+
queryParams.name = options.name;
|
|
444
|
+
}
|
|
445
|
+
// Fetch blueprints
|
|
446
|
+
const page = (await client.blueprints.list(queryParams));
|
|
447
|
+
// Extract blueprints array
|
|
448
|
+
const blueprints = page.blueprints || [];
|
|
449
|
+
output(blueprints, { format: options.output, defaultFormat: "json" });
|
|
450
|
+
}
|
|
451
|
+
catch (error) {
|
|
452
|
+
outputError("Failed to list blueprints", error);
|
|
453
|
+
}
|
|
452
454
|
}
|