@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.
Files changed (96) hide show
  1. package/README.md +54 -0
  2. package/dist/cli.js +73 -60
  3. package/dist/commands/auth.js +0 -1
  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 +215 -213
  7. package/dist/commands/blueprint/logs.js +133 -37
  8. package/dist/commands/blueprint/preview.js +42 -38
  9. package/dist/commands/config.js +117 -0
  10. package/dist/commands/devbox/create.js +120 -40
  11. package/dist/commands/devbox/delete.js +17 -33
  12. package/dist/commands/devbox/download.js +29 -43
  13. package/dist/commands/devbox/exec.js +22 -39
  14. package/dist/commands/devbox/execAsync.js +20 -37
  15. package/dist/commands/devbox/get.js +13 -35
  16. package/dist/commands/devbox/getAsync.js +12 -34
  17. package/dist/commands/devbox/list.js +241 -402
  18. package/dist/commands/devbox/logs.js +20 -38
  19. package/dist/commands/devbox/read.js +29 -43
  20. package/dist/commands/devbox/resume.js +13 -35
  21. package/dist/commands/devbox/rsync.js +26 -78
  22. package/dist/commands/devbox/scp.js +25 -79
  23. package/dist/commands/devbox/sendStdin.js +41 -0
  24. package/dist/commands/devbox/shutdown.js +13 -35
  25. package/dist/commands/devbox/ssh.js +45 -78
  26. package/dist/commands/devbox/suspend.js +13 -35
  27. package/dist/commands/devbox/tunnel.js +36 -88
  28. package/dist/commands/devbox/upload.js +28 -36
  29. package/dist/commands/devbox/write.js +29 -44
  30. package/dist/commands/mcp-install.js +4 -3
  31. package/dist/commands/menu.js +24 -66
  32. package/dist/commands/object/delete.js +12 -34
  33. package/dist/commands/object/download.js +26 -74
  34. package/dist/commands/object/get.js +12 -34
  35. package/dist/commands/object/list.js +15 -93
  36. package/dist/commands/object/upload.js +35 -96
  37. package/dist/commands/snapshot/create.js +23 -39
  38. package/dist/commands/snapshot/delete.js +17 -33
  39. package/dist/commands/snapshot/get.js +16 -0
  40. package/dist/commands/snapshot/list.js +309 -80
  41. package/dist/commands/snapshot/status.js +12 -34
  42. package/dist/components/ActionsPopup.js +63 -39
  43. package/dist/components/Breadcrumb.js +10 -52
  44. package/dist/components/DevboxActionsMenu.js +182 -110
  45. package/dist/components/DevboxCreatePage.js +12 -7
  46. package/dist/components/DevboxDetailPage.js +76 -28
  47. package/dist/components/ErrorBoundary.js +29 -0
  48. package/dist/components/ErrorMessage.js +10 -2
  49. package/dist/components/Header.js +12 -4
  50. package/dist/components/InteractiveSpawn.js +94 -0
  51. package/dist/components/MainMenu.js +36 -32
  52. package/dist/components/MetadataDisplay.js +4 -4
  53. package/dist/components/OperationsMenu.js +1 -1
  54. package/dist/components/ResourceActionsMenu.js +4 -4
  55. package/dist/components/ResourceListView.js +46 -34
  56. package/dist/components/Spinner.js +7 -2
  57. package/dist/components/StatusBadge.js +1 -1
  58. package/dist/components/SuccessMessage.js +12 -2
  59. package/dist/components/Table.js +16 -6
  60. package/dist/hooks/useCursorPagination.js +125 -85
  61. package/dist/hooks/useExitOnCtrlC.js +14 -0
  62. package/dist/hooks/useViewportHeight.js +47 -0
  63. package/dist/mcp/server.js +65 -6
  64. package/dist/router/Router.js +68 -0
  65. package/dist/router/types.js +1 -0
  66. package/dist/screens/BlueprintListScreen.js +7 -0
  67. package/dist/screens/DevboxActionsScreen.js +25 -0
  68. package/dist/screens/DevboxCreateScreen.js +11 -0
  69. package/dist/screens/DevboxDetailScreen.js +60 -0
  70. package/dist/screens/DevboxListScreen.js +23 -0
  71. package/dist/screens/LogsSessionScreen.js +49 -0
  72. package/dist/screens/MenuScreen.js +23 -0
  73. package/dist/screens/SSHSessionScreen.js +55 -0
  74. package/dist/screens/SnapshotListScreen.js +7 -0
  75. package/dist/services/blueprintService.js +105 -0
  76. package/dist/services/devboxService.js +215 -0
  77. package/dist/services/snapshotService.js +81 -0
  78. package/dist/store/blueprintStore.js +89 -0
  79. package/dist/store/devboxStore.js +105 -0
  80. package/dist/store/index.js +7 -0
  81. package/dist/store/navigationStore.js +101 -0
  82. package/dist/store/snapshotStore.js +87 -0
  83. package/dist/utils/CommandExecutor.js +53 -24
  84. package/dist/utils/client.js +0 -2
  85. package/dist/utils/config.js +20 -90
  86. package/dist/utils/interactiveCommand.js +3 -2
  87. package/dist/utils/logFormatter.js +162 -0
  88. package/dist/utils/memoryMonitor.js +85 -0
  89. package/dist/utils/output.js +150 -59
  90. package/dist/utils/screen.js +23 -0
  91. package/dist/utils/ssh.js +3 -1
  92. package/dist/utils/sshSession.js +5 -29
  93. package/dist/utils/terminalDetection.js +97 -0
  94. package/dist/utils/terminalSync.js +39 -0
  95. package/dist/utils/theme.js +147 -13
  96. package/package.json +16 -13
@@ -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,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 { 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
+ 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 [loading, setLoading] = React.useState(false);
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 responsive column widths
40
- const terminalWidth = stdout?.columns || 120;
41
- const showDescription = terminalWidth >= 120;
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
- // Fetch blueprints - moved to top to ensure hooks are called in same order
175
+ // Handle Ctrl+C to exit
176
+ useExitOnCtrlC();
177
+ // Ensure selected index is within bounds
72
178
  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);
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
- finally {
91
- setLoading(false);
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
- fetchBlueprints();
95
- }, []);
96
- // Handle input for all views - combined into single hook
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; // Let DevboxCreatePage handle its own input
253
+ return;
132
254
  }
133
- // Handle actions popup overlay: consume keys and prevent table nav
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 (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;
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
- currentPage < totalPages - 1) {
211
- setCurrentPage(currentPage + 1);
310
+ !loading &&
311
+ !navigating &&
312
+ hasMore) {
313
+ nextPage();
212
314
  setSelectedIndex(0);
213
315
  }
214
- else if ((input === "p" || key.leftArrow) && currentPage > 0) {
215
- setCurrentPage(currentPage - 1);
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" && currentBlueprints[selectedIndex]) {
224
- // Open in browser
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
- 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;
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 (loading) {
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: (devbox) => {
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 }], showVersionCheck: true }), _jsx(SpinnerComponent, { message: "Loading blueprints..." })] }));
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 }], showVersionCheck: true }), _jsx(ErrorMessage, { message: "Failed to load blueprints", error: listError })] }));
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 }], 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" })] })] }));
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: 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) => ({
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"] }), 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"] })] })] }));
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
- 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);
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
  }