@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.
Files changed (105) hide show
  1. package/README.md +54 -10
  2. package/dist/cli.js +79 -72
  3. package/dist/commands/auth.js +2 -2
  4. package/dist/commands/blueprint/create.js +31 -83
  5. package/dist/commands/blueprint/get.js +29 -34
  6. package/dist/commands/blueprint/list.js +278 -230
  7. package/dist/commands/blueprint/logs.js +133 -37
  8. package/dist/commands/config.js +118 -0
  9. package/dist/commands/devbox/create.js +120 -40
  10. package/dist/commands/devbox/delete.js +17 -33
  11. package/dist/commands/devbox/download.js +29 -43
  12. package/dist/commands/devbox/exec.js +22 -39
  13. package/dist/commands/devbox/execAsync.js +20 -37
  14. package/dist/commands/devbox/get.js +13 -35
  15. package/dist/commands/devbox/getAsync.js +12 -34
  16. package/dist/commands/devbox/list.js +241 -402
  17. package/dist/commands/devbox/logs.js +20 -38
  18. package/dist/commands/devbox/read.js +29 -43
  19. package/dist/commands/devbox/resume.js +13 -35
  20. package/dist/commands/devbox/rsync.js +26 -78
  21. package/dist/commands/devbox/scp.js +25 -79
  22. package/dist/commands/devbox/sendStdin.js +41 -0
  23. package/dist/commands/devbox/shutdown.js +13 -35
  24. package/dist/commands/devbox/ssh.js +46 -78
  25. package/dist/commands/devbox/suspend.js +13 -35
  26. package/dist/commands/devbox/tunnel.js +37 -88
  27. package/dist/commands/devbox/upload.js +28 -36
  28. package/dist/commands/devbox/write.js +29 -44
  29. package/dist/commands/mcp-http.js +6 -5
  30. package/dist/commands/mcp-install.js +12 -10
  31. package/dist/commands/mcp.js +5 -4
  32. package/dist/commands/menu.js +26 -67
  33. package/dist/commands/object/delete.js +12 -34
  34. package/dist/commands/object/download.js +26 -74
  35. package/dist/commands/object/get.js +12 -34
  36. package/dist/commands/object/list.js +15 -93
  37. package/dist/commands/object/upload.js +35 -96
  38. package/dist/commands/snapshot/create.js +23 -39
  39. package/dist/commands/snapshot/delete.js +17 -33
  40. package/dist/commands/snapshot/get.js +16 -0
  41. package/dist/commands/snapshot/list.js +309 -80
  42. package/dist/commands/snapshot/status.js +12 -34
  43. package/dist/components/ActionsPopup.js +64 -39
  44. package/dist/components/Banner.js +7 -1
  45. package/dist/components/Breadcrumb.js +11 -48
  46. package/dist/components/DevboxActionsMenu.js +117 -207
  47. package/dist/components/DevboxCreatePage.js +12 -7
  48. package/dist/components/DevboxDetailPage.js +76 -28
  49. package/dist/components/ErrorBoundary.js +29 -0
  50. package/dist/components/ErrorMessage.js +10 -2
  51. package/dist/components/Header.js +12 -4
  52. package/dist/components/InteractiveSpawn.js +104 -0
  53. package/dist/components/LogsViewer.js +169 -0
  54. package/dist/components/MainMenu.js +37 -33
  55. package/dist/components/MetadataDisplay.js +4 -4
  56. package/dist/components/OperationsMenu.js +1 -1
  57. package/dist/components/ResourceActionsMenu.js +4 -4
  58. package/dist/components/ResourceListView.js +46 -34
  59. package/dist/components/Spinner.js +7 -2
  60. package/dist/components/StatusBadge.js +1 -1
  61. package/dist/components/SuccessMessage.js +12 -2
  62. package/dist/components/Table.js +16 -6
  63. package/dist/components/UpdateNotification.js +56 -0
  64. package/dist/hooks/useCursorPagination.js +125 -85
  65. package/dist/hooks/useExitOnCtrlC.js +15 -0
  66. package/dist/hooks/useViewportHeight.js +47 -0
  67. package/dist/mcp/server-http.js +2 -1
  68. package/dist/mcp/server.js +71 -7
  69. package/dist/router/Router.js +70 -0
  70. package/dist/router/types.js +1 -0
  71. package/dist/screens/BlueprintListScreen.js +7 -0
  72. package/dist/screens/BlueprintLogsScreen.js +74 -0
  73. package/dist/screens/DevboxActionsScreen.js +25 -0
  74. package/dist/screens/DevboxCreateScreen.js +11 -0
  75. package/dist/screens/DevboxDetailScreen.js +60 -0
  76. package/dist/screens/DevboxListScreen.js +23 -0
  77. package/dist/screens/LogsSessionScreen.js +49 -0
  78. package/dist/screens/MenuScreen.js +23 -0
  79. package/dist/screens/SSHSessionScreen.js +55 -0
  80. package/dist/screens/SnapshotListScreen.js +7 -0
  81. package/dist/services/blueprintService.js +101 -0
  82. package/dist/services/devboxService.js +215 -0
  83. package/dist/services/snapshotService.js +81 -0
  84. package/dist/store/blueprintStore.js +89 -0
  85. package/dist/store/devboxStore.js +105 -0
  86. package/dist/store/index.js +7 -0
  87. package/dist/store/navigationStore.js +101 -0
  88. package/dist/store/snapshotStore.js +87 -0
  89. package/dist/utils/client.js +4 -2
  90. package/dist/utils/config.js +22 -111
  91. package/dist/utils/interactiveCommand.js +3 -2
  92. package/dist/utils/logFormatter.js +208 -0
  93. package/dist/utils/memoryMonitor.js +85 -0
  94. package/dist/utils/output.js +153 -61
  95. package/dist/utils/process.js +106 -0
  96. package/dist/utils/processUtils.js +135 -0
  97. package/dist/utils/screen.js +61 -0
  98. package/dist/utils/ssh.js +6 -3
  99. package/dist/utils/sshSession.js +5 -29
  100. package/dist/utils/terminalDetection.js +185 -0
  101. package/dist/utils/terminalSync.js +39 -0
  102. package/dist/utils/theme.js +162 -13
  103. package/dist/utils/versionCheck.js +53 -0
  104. package/dist/version.js +12 -0
  105. package/package.json +19 -17
@@ -1,6 +1,6 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
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, useStdout } from "ink";
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 { createExecutor } from "../../utils/CommandExecutor.js";
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
- const PAGE_SIZE = 10;
21
- const MAX_FETCH = 100;
22
- const ListBlueprintsUI = ({ onBack, onExit }) => {
23
- const { stdout } = useStdout();
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 [loading, setLoading] = React.useState(false);
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
- // Calculate responsive column widths
40
- const terminalWidth = stdout?.columns || 120;
41
- const showDescription = terminalWidth >= 120;
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
- // Only show create devbox option if blueprint is successfully built
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
- // Fetch blueprints - moved to top to ensure hooks are called in same order
184
+ // Handle Ctrl+C to exit
185
+ useExitOnCtrlC();
186
+ // Ensure selected index is within bounds
72
187
  React.useEffect(() => {
73
- const fetchBlueprints = async () => {
74
- try {
75
- setLoading(true);
76
- const client = getClient();
77
- const allBlueprints = [];
78
- let count = 0;
79
- for await (const blueprint of client.blueprints.list()) {
80
- allBlueprints.push(blueprint);
81
- count++;
82
- if (count >= MAX_FETCH)
83
- break;
84
- }
85
- setBlueprints(allBlueprints);
86
- }
87
- catch (err) {
88
- setListError(err);
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
- finally {
91
- setLoading(false);
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
- fetchBlueprints();
95
- }, []);
96
- // Handle input for all views - combined into single hook
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; // Let DevboxCreatePage handle its own input
284
+ return;
132
285
  }
133
- // Handle actions popup overlay: consume keys and prevent table nav
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
- 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);
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 (default view)
197
- const pageSize = PAGE_SIZE;
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
- currentPage < totalPages - 1) {
211
- setCurrentPage(currentPage + 1);
350
+ !loading &&
351
+ !navigating &&
352
+ hasMore) {
353
+ nextPage();
212
354
  setSelectedIndex(0);
213
355
  }
214
- else if ((input === "p" || key.leftArrow) && currentPage > 0) {
215
- setCurrentPage(currentPage - 1);
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 === "o" && currentBlueprints[selectedIndex]) {
224
- // Open in browser
225
- const url = getBlueprintUrl(currentBlueprints[selectedIndex].id);
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
- catch (err) {
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 (loading) {
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 (!needsInput) {
332
- const messages = {
333
- delete: "Deleting blueprint...",
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: "Executing Operation" }), _jsx(SpinnerComponent, { message: messages[executingOperation] || "Please wait..." })] }));
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
- return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
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: (devbox) => {
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 }], showVersionCheck: true }), _jsx(SpinnerComponent, { message: "Loading blueprints..." })] }));
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 }], showVersionCheck: true }), _jsx(ErrorMessage, { message: "Failed to load blueprints", error: listError })] }));
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 }], showVersionCheck: 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" })] })] }));
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: currentBlueprints, keyExtractor: (blueprint) => blueprint.id, selectedIndex: selectedIndex, title: `blueprints[${blueprints.length}]`, columns: [
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
- })), 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"] }), totalPages > 1 && (_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"] })] })] }));
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
- const executor = createExecutor(options);
446
- await executor.executeList(async () => {
447
- const client = executor.getClient();
448
- return executor.fetchFromIterator(client.blueprints.list(), {
449
- limit: PAGE_SIZE,
450
- });
451
- }, () => _jsx(ListBlueprintsUI, {}), PAGE_SIZE);
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
  }